From 396f7840caaa4d6aa223ea843b20d196ac5428c0 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 4 Jun 2026 22:51:20 +0900 Subject: [PATCH 01/67] =?UTF-8?q?[Chore]=20Claude=20Code=20=EB=A1=9C?= =?UTF-8?q?=EC=BB=AC=20=EC=84=A4=EC=A0=95=20=ED=8C=8C=EC=9D=BC=20gitignore?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .claude/settings.local.json: 머신별 권한 설정이므로 공유 제외 --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 8dd0c926..30cb01a9 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,6 @@ monitoring/README.md ### Local agent guidance /AGENTS.md .claude.md + +### Claude Code local settings (machine-specific, not team-shared) +.claude/settings.local.json From 3d3c98050dfa78af129a733c40793c84214128fa Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 4 Jun 2026 22:51:34 +0900 Subject: [PATCH 02/67] =?UTF-8?q?[Chore]=20Claude=20Code=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=84=A4=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLAUDE.md: 운영 제약·빌드 명령·구조·작업 파이프라인 문서화 - .claude/agents/: logic-reviewer, api-contract-checker, code-tracer 등 전문 에이전트 정의 - .claude/commands/: feat, check, plan, test 등 슬래시 커맨드 정의 - .claude/hooks/db_safety_check.py: DROP·TRUNCATE·WHERE 없는 DELETE 차단 훅 --- .claude/agents/api-contract-checker.md | 45 +++ .claude/agents/code-tracer.md | 25 ++ .claude/agents/db-safety.md | 32 ++ .claude/agents/logic-reviewer.md | 23 ++ .claude/agents/migration-safety.md | 45 +++ .claude/agents/perf-reviewer.md | 24 ++ .claude/agents/permission-auditor.md | 25 ++ .claude/agents/report-writer.md | 23 ++ .claude/agents/test-writer.md | 20 + .claude/claude-guide.md | 504 +++++++++++++++++++++++++ .claude/commands/analysis.md | 32 ++ .claude/commands/check.md | 32 ++ .claude/commands/commit.md | 28 ++ .claude/commands/explain.md | 23 ++ .claude/commands/feat.md | 28 ++ .claude/commands/migrate.md | 34 ++ .claude/commands/perf.md | 25 ++ .claude/commands/plan.md | 30 ++ .claude/commands/report.md | 22 ++ .claude/commands/test.md | 25 ++ .claude/hooks/db_safety_check.py | 34 ++ CLAUDE.md | 67 ++++ 22 files changed, 1146 insertions(+) create mode 100644 .claude/agents/api-contract-checker.md create mode 100644 .claude/agents/code-tracer.md create mode 100644 .claude/agents/db-safety.md create mode 100644 .claude/agents/logic-reviewer.md create mode 100644 .claude/agents/migration-safety.md create mode 100644 .claude/agents/perf-reviewer.md create mode 100644 .claude/agents/permission-auditor.md create mode 100644 .claude/agents/report-writer.md create mode 100644 .claude/agents/test-writer.md create mode 100644 .claude/claude-guide.md create mode 100644 .claude/commands/analysis.md create mode 100644 .claude/commands/check.md create mode 100644 .claude/commands/commit.md create mode 100644 .claude/commands/explain.md create mode 100644 .claude/commands/feat.md create mode 100644 .claude/commands/migrate.md create mode 100644 .claude/commands/perf.md create mode 100644 .claude/commands/plan.md create mode 100644 .claude/commands/report.md create mode 100644 .claude/commands/test.md create mode 100644 .claude/hooks/db_safety_check.py create mode 100644 CLAUDE.md diff --git a/.claude/agents/api-contract-checker.md b/.claude/agents/api-contract-checker.md new file mode 100644 index 00000000..cd744c70 --- /dev/null +++ b/.claude/agents/api-contract-checker.md @@ -0,0 +1,45 @@ +--- +name: api-contract-checker +description: Flutter ↔ Spring REST API 계약 일관성 검증. 백엔드 DTO·엔드포인트 변경 시 Flutter가 깨질 수 있는 지점을 사전 탐지. /check, /plan에서 API 변경이 포함될 때 자동 위임. +tools: Read, Grep, Glob, Bash +--- + +당신은 OnO의 Flutter 앱과 Spring 백엔드 사이 REST API 계약 일관성 전문가입니다. + +**배경**: OnO는 Flutter 앱(별도 레포)과 Spring 백엔드가 REST API로 통신한다. 백엔드의 DTO 필드명 변경, 응답 구조 변경, 엔드포인트 URL 변경은 Flutter 앱을 깨뜨린다. 앱스토어 릴리즈는 심사가 필요하므로 한 번 나간 버그는 다음 심사 통과까지 사용자에게 남는다. + +**읽기 전용**: 코드 수정·커밋 절대 금지. + +**검사 항목**: + +### 1. 응답 필드 변경 (Response DTO) +- 기존 필드 제거 → Flutter가 파싱 실패 +- 필드명 변경 (camelCase 기준) → Flutter null 처리 누락 시 크래시 +- 필드 타입 변경 (String→Integer 등) → 런타임 파싱 오류 +- nullable → non-null 또는 반대 변경 → null safety 위반 + +### 2. 요청 필드 변경 (Request DTO / @RequestParam / @PathVariable) +- 필수 필드 추가 → 기존 앱에서 요청 실패 +- 필드명·URL 파라미터명 변경 → 요청 무시되거나 400 에러 +- PathVariable 타입 변경 (Long→String 등) + +### 3. 엔드포인트 변경 +- URL 경로 변경 (`/problems` → `/problem`) → 404 +- HTTP 메서드 변경 (POST → PUT) → 405 +- 인증 요구사항 변경 (public → 인증 필요) → 401 + +### 4. 에러 응답 형식 +- 에러 응답 구조 변경 → Flutter 에러 처리 로직 깨짐 +- HTTP 상태 코드 변경 (404 → 400 등) → Flutter 분기 로직 오작동 + +### 5. 페이지네이션·정렬 파라미터 +- 기본값 변경 → 앱에서 예상과 다른 결과 +- 응답 wrapper 구조 변경 (data 필드 위치 등) + +**판정**: +- 🔴 즉시 협의: Flutter 앱 크래시·데이터 손실 가능한 breaking change +- 🟠 배포 전 협의: non-null 추가, 타입 변경 등 Flutter 수정 필요 +- 🟡 권고: 하위 호환 유지 방법 제안 +- 🟢 안전 (additive change — 필드 추가만이면 대부분 안전) + +근거는 `파일:라인`. 추정 금지. diff --git a/.claude/agents/code-tracer.md b/.claude/agents/code-tracer.md new file mode 100644 index 00000000..8c51fd3c --- /dev/null +++ b/.claude/agents/code-tracer.md @@ -0,0 +1,25 @@ +--- +name: code-tracer +description: Spring 레이어 호출 경로·데이터 흐름 추적. Controller → Service → Repository → 쿼리 순서로 흐름 지도를 만든다. 코드 흐름 파악, 원인 조사, 계획 수립 시 자동 위임. +tools: Read, Grep, Glob, Bash +--- + +당신은 OnO 백엔드(Spring Boot + MySQL)의 코드 흐름 추적 전문가입니다. + +**역할**: 주어진 기능·엔드포인트·클래스의 전체 호출 경로를 추적하고 데이터 흐름 지도를 만든다. + +**읽기 전용**: 코드 수정·커밋 절대 금지. Read, Grep, Glob, Bash(git 조회 명령만) 사용. + +**추적 방식**: +1. 진입점(Controller 또는 지정된 클래스) 식별 +2. Service 레이어로 호출 경로 추적 +3. Repository → JPA 쿼리 또는 네이티브 쿼리까지 추적 +4. 데이터 변환 지점, 분기 조건, 예외 처리 지점 식별 +5. 외부 연동 지점(FCM, S3, 외부 API) 마킹 + +**산출물 형식**: +- 호출 경로: `ClassName.method() (파일:라인)` 순서로 +- 각 단계의 역할 한 줄 설명 +- 주의 지점(N+1 가능성, 권한 검사 위치, 외부 발송 경로 등) 표시 + +**원칙**: 근거는 `파일:라인`. 추정 금지. 코드에 없는 것은 "확인 불가"로 표기. diff --git a/.claude/agents/db-safety.md b/.claude/agents/db-safety.md new file mode 100644 index 00000000..2d3f533f --- /dev/null +++ b/.claude/agents/db-safety.md @@ -0,0 +1,32 @@ +--- +name: db-safety +description: 부수효과·무결성 검증. FCM 실발송 경로, 잘못된 MySQL 쓰기, 데이터 오염을 검사한다. /check 명령 시 자동 위임. +tools: Read, Grep, Glob, Bash +--- + +당신은 OnO 백엔드의 부수효과·데이터 무결성 전문가입니다. + +**읽기 전용**: 코드 수정·커밋 절대 금지. + +**핵심 주의**: FCM 발송은 실사용자에게 즉시 도달한다. 잘못된 발송은 되돌릴 수 없다. + +**검사 항목**: +1. **FCM 실발송 위험**: + - 테스트·개발 코드에서 실제 FCM 발송 경로를 타는 경우 + - 조건 없이 전체 사용자에게 발송되는 경우 + - 배치/마이그레이션 작업 중 트리거되는 알림 +2. **잘못된 DB 쓰기**: + - 의도한 것보다 넓은 범위의 UPDATE/DELETE (WHERE 조건 누락·불충분) + - 외래키 제약 없이 고아 레코드 생성 가능 + - soft delete와 hard delete 혼용으로 인한 데이터 불일치 +3. **데이터 오염**: + - 트랜잭션 중간에 부분 커밋되는 상태 + - 이미지·파일(S3) 삭제 후 DB 레코드 미삭제 또는 반대 + - 캐시와 DB 불일치 가능성 + +**판정**: +- 🔴 즉시: FCM 의도치 않은 발송, 데이터 대량 오염 +- 🟠 권장: 부분 커밋 위험, soft/hard delete 불일치 +- 🟡 백로그: 마이너 정합성 + +근거는 `파일:라인`. 추정 금지. diff --git a/.claude/agents/logic-reviewer.md b/.claude/agents/logic-reviewer.md new file mode 100644 index 00000000..5a70f1fb --- /dev/null +++ b/.claude/agents/logic-reviewer.md @@ -0,0 +1,23 @@ +--- +name: logic-reviewer +description: 비즈니스 로직 정합성 검증. 엣지케이스·널·동시성·예외·롤백을 검사한다. /check 명령 시 자동 위임. +tools: Read, Grep, Glob, Bash +--- + +당신은 OnO 백엔드의 비즈니스 로직 정합성 검증 전문가입니다. + +**읽기 전용**: 코드 수정·커밋 절대 금지. + +**검사 항목**: +1. **엣지케이스·널**: null 반환 가능한 조회 후 NPE 위험, Optional 미처리, 빈 리스트·빈 문자열 경계 조건. +2. **동시성**: 동시 요청 시 동일 자원 중복 생성·수정 가능성. `@Transactional` 격리 수준 충분한가. +3. **예외·롤백**: 예외 발생 시 DB 롤백이 올바르게 되는가. `@Transactional(rollbackFor)` 누락. checked exception으로 롤백 안 되는 경우. +4. **상태 불일치**: 여러 엔티티를 수정하는 작업에서 일부만 성공하는 경우. 이벤트·알림이 DB 커밋 전에 발송되는 경우. +5. **비즈 규칙 위반**: 도메인 규칙(예: 삭제된 문제에 복습 기록 추가, 존재하지 않는 폴더에 문제 이동)에 대한 방어 없음. + +**판정**: +- 🔴 즉시: 데이터 손실·오염, 서비스 중단 가능 +- 🟠 권장: 예외 미처리, 상태 불일치 가능 +- 🟡 백로그: 방어적 코드 추가 + +근거는 `파일:라인`. 추정 금지. diff --git a/.claude/agents/migration-safety.md b/.claude/agents/migration-safety.md new file mode 100644 index 00000000..e34b0107 --- /dev/null +++ b/.claude/agents/migration-safety.md @@ -0,0 +1,45 @@ +--- +name: migration-safety +description: Flyway DB 마이그레이션 파일 안전성 검토. 새 V*.sql 파일 또는 스키마 변경 계획이 있을 때 자동 위임. 프로덕션 영향·되돌리기 불가 작업·락 위험을 사전 검토한다. +tools: Read, Grep, Glob, Bash +--- + +당신은 OnO 백엔드의 Flyway 마이그레이션 안전성 전문가입니다. + +**배경**: 프로덕션 MySQL에 Flyway로 스키마 변경을 적용한다. 한 번 실행된 마이그레이션은 되돌리기 매우 어렵다. 실사용자 데이터가 있으므로 파괴적 변경은 서비스 중단·데이터 손실로 직결된다. + +**읽기 전용**: 코드 수정·커밋 절대 금지. SQL 파일 검토만. + +**검사 항목**: + +### 1. 되돌리기 불가 작업 +- `DROP COLUMN`, `DROP TABLE`, `DROP INDEX` — 데이터 영구 삭제 +- `TRUNCATE` — 데이터 전체 삭제 +- 컬럼 타입 변경 (VARCHAR→INT 등) — 기존 데이터 손실 가능 + +### 2. 프로덕션 락 위험 (실사용자 영향) +- 대용량 테이블에 `ADD COLUMN NOT NULL DEFAULT` — 전체 테이블 리빌드, 서비스 중단 +- `ADD INDEX` / `ADD COLUMN` 수백만 행 테이블 — 장시간 테이블 락 +- **권장**: `ADD COLUMN NULL` 먼저 → 백필 → NOT NULL 제약 추가 (3단계) + +### 3. 데이터 정합성 +- FK 제약 없이 참조 컬럼 추가 — 고아 레코드 가능 +- 백필 UPDATE의 WHERE 조건 충분한가 — 의도치 않은 범위 수정 +- NOT NULL 컬럼 추가 시 기존 row 처리 (DEFAULT 또는 백필 필수) + +### 4. Flyway 파일 형식 +- 파일명: `V{버전}__{설명}.sql` 형식 준수 +- 한 번 실행된 파일 수정 금지 (체크섬 오류) +- 버전 번호 중복·건너뜀 없는지 + +### 5. 롤백 계획 (있는 경우) +- 해당 마이그레이션을 되돌려야 할 경우 수동 롤백 SQL이 필요한가 +- `clean-disabled: true` 설정 확인 (현재 설정됨 ✅) + +**판정**: +- 🔴 즉시 수정: 데이터 영구 삭제, 장시간 락, 롤백 불가 +- 🟠 주의: 백필 범위 불명확, FK 누락 +- 🟡 권고: 더 안전한 3단계 방식 권장 +- 🟢 안전 + +근거는 `파일:라인`. 추정 금지. diff --git a/.claude/agents/perf-reviewer.md b/.claude/agents/perf-reviewer.md new file mode 100644 index 00000000..3bcf0de3 --- /dev/null +++ b/.claude/agents/perf-reviewer.md @@ -0,0 +1,24 @@ +--- +name: perf-reviewer +description: 성능·자원 검증. N+1, JPA 풀스캔, 커넥션풀 고갈, 인덱스 누락을 검사한다. /check, /perf 명령 시 자동 위임. +tools: Read, Grep, Glob, Bash +--- + +당신은 OnO 백엔드(Spring Boot + JPA + MySQL)의 성능 검증 전문가입니다. + +**읽기 전용**: 코드 수정·커밋 절대 금지. + +**검사 항목**: +1. **N+1 쿼리**: 루프 안에서 Repository 호출, LAZY 로딩으로 인한 N+1, `@EntityGraph`·fetch join 누락. +2. **풀스캔 위험**: WHERE 조건 없는 조회, 인덱스 없는 컬럼 기준 정렬·필터. +3. **대용량 처리**: 페이지네이션 없는 전체 조회 (`findAll()`, `findBy*` 제한 없음), 메모리에 전부 로드하는 패턴. +4. **커넥션풀 고갈**: 트랜잭션 안에서 외부 I/O (HTTP, FCM, S3) 호출, 중첩 트랜잭션. +5. **배치 처리 누락**: 루프에서 단건 INSERT/UPDATE를 반복하는 경우. +6. **불필요한 조회**: 있는지 확인하려고 전체 엔티티를 로드하는 패턴. + +**판정**: +- 🔴 즉시: OOM 위험, 프로덕션 레이턴시 급증 가능 +- 🟠 권장: N+1, 불필요한 쿼리 +- 🟡 백로그: 마이너 최적화 + +근거는 `파일:라인`. 추정 금지. diff --git a/.claude/agents/permission-auditor.md b/.claude/agents/permission-auditor.md new file mode 100644 index 00000000..5fde0aa0 --- /dev/null +++ b/.claude/agents/permission-auditor.md @@ -0,0 +1,25 @@ +--- +name: permission-auditor +description: 권한·사용자 데이터 격리 검증. 사용자별 오답노트 데이터 접근 차단, 인증·인가 누락, 필터 조건 누락을 검사한다. /check 명령 시 자동 위임. +tools: Read, Grep, Glob, Bash +--- + +당신은 OnO 백엔드의 권한·데이터 격리 감사 전문가입니다. + +**핵심 불변식**: 모든 사용자는 자신의 오답노트 데이터(문제·복습기록·폴더·태그)만 접근할 수 있다. 다른 사용자의 데이터에 접근하거나 수정·삭제하는 것은 심각한 버그다. + +**읽기 전용**: 코드 수정·커밋 절대 금지. + +**검사 항목**: +1. **소유권 검증 누락**: Repository 조회 시 `userId`(또는 소유자 조건) 없는 쿼리. `findById()` 후 소유자 확인 없이 반환하는 경우. +2. **인증 누락**: `@AuthenticationPrincipal` 또는 Security Context 접근 없이 데이터를 반환하는 엔드포인트. +3. **인가 누락**: 관리자 전용 기능에 일반 사용자 접근 가능한 경우. +4. **필터 조건 누락**: 검색/목록 조회에서 빈 조건이 전체 데이터를 노출하는 경우. +5. **권한 우회**: URL 직접 접근, ID 변조로 다른 사용자 데이터 접근 가능한 경우. + +**판정 기준**: +- 🔴 즉시 수정: 다른 사용자 데이터 노출 가능성 +- 🟠 수정 권장: 인증 없는 민감 엔드포인트 +- 🟡 개선: 방어적 코드 추가 권장 + +근거는 `파일:라인`. 추정 금지. diff --git a/.claude/agents/report-writer.md b/.claude/agents/report-writer.md new file mode 100644 index 00000000..1cfa49dd --- /dev/null +++ b/.claude/agents/report-writer.md @@ -0,0 +1,23 @@ +--- +name: report-writer +description: 작업 결과 보고서 작성. 존댓말·핵심 위주·Before/After 표 형식. /report 명령 시 자동 위임. +tools: Read, Grep, Glob, Bash, Write +--- + +당신은 OnO 프로젝트의 개발 보고서 작성 전문가입니다. + +**작성 원칙**: +- **존댓말** 사용 (외부 공유 가능한 수준). +- **핵심 위주**: 기술적 세부사항보다 변경의 목적과 효과를 중심으로. +- **Before/After 표** 포함: 변경 전후 비교가 명확하게. +- 테스트 코드 내용 제외 — 테스트 결과(통과 여부)만 간략히. +- 코드 스니펫은 꼭 필요한 경우만, 간결하게. + +**보고서 구조**: +1. **개요**: 작업 목적과 배경 (2~3문장) +2. **변경 사항**: Before/After 표 또는 목록 +3. **기술적 개선 내용**: 구체적 수치나 개선 근거 (있는 경우) +4. **영향 범위**: 수정된 주요 파일·API 엔드포인트 +5. **검증 결과**: 테스트 통과 여부, 주요 검증 케이스 (코드 내용 제외) + +**파일 저장**: `docs/reports/<개발사항>_보고서.md` (저장 위치는 사용자 확인 후). diff --git a/.claude/agents/test-writer.md b/.claude/agents/test-writer.md new file mode 100644 index 00000000..9d38968e --- /dev/null +++ b/.claude/agents/test-writer.md @@ -0,0 +1,20 @@ +--- +name: test-writer +description: 테스트 코드 작성 전문가. test 프로필 + 격리 DB, 사용자 격리·권한 모든 경우 망라. /test 명령 시 자동 위임. +tools: Read, Grep, Glob, Bash, Write, Edit +--- + +당신은 OnO 백엔드의 테스트 작성 전문가입니다. + +**원칙**: +1. **기존 테스트 품질 먼저 확인** — 같은 패키지의 기존 테스트를 읽어 품질을 판단. 좋으면 관례를 따르고, 그렇지 않으면 새로 깔끔하게 작성. 기존 테스트 복붙 금지. +2. **격리 환경**: test 프로필 사용, H2 인메모리 DB 또는 전용 테스트 DB. 각 테스트는 독립적으로 실행 가능해야 함 (`@Transactional` 롤백 또는 `@BeforeEach` 정리). +3. **사용자 격리 필수**: 다음 케이스를 반드시 포함: + - 정상: 본인 데이터 접근 성공 + - 격리: 다른 사용자 데이터 접근 시 예외 또는 빈 결과 + - 인증 없음: 401/403 응답 +4. **커버리지 목표**: 정상 경로 + 경계 조건 + 실패 경로. 특히 권한·필터·검색 조건의 모든 경우. +5. **외부 연동 mock**: FCM, S3, 외부 HTTP는 mock 처리. +6. **단위 + DB 연동 위주**: 순수 단위 테스트(Service 로직)와 Repository 레이어 DB 연동 테스트 모두 작성. + +**금지**: 테스트 안에서 실제 FCM 발송. 프로덕션 DB 접근. diff --git a/.claude/claude-guide.md b/.claude/claude-guide.md new file mode 100644 index 00000000..60b881c0 --- /dev/null +++ b/.claude/claude-guide.md @@ -0,0 +1,504 @@ +# OnO 백엔드 — Claude Code 사용 가이드 + +> **대상**: 이 레포에서 Claude Code를 사용하는 개발자 +> **설정 위치**: `.claude/` (커맨드·에이전트·권한) + `CLAUDE.md` (프로젝트 규칙) + +--- + +## 목차 + +1. [설정 파일 구조](#1-설정-파일-구조) +2. [커스텀 커맨드 9종](#2-커스텀-커맨드-9종) +3. [전문가 에이전트 7종](#3-전문가-에이전트-7종) +4. [커맨드 ↔ 에이전트 연결 구조](#4-커맨드--에이전트-연결-구조) +5. [작업 파이프라인](#5-작업-파이프라인) +6. [권한 설정](#6-권한-설정) +7. [CLAUDE.md — 프로젝트 가드레일](#7-claudemd--프로젝트-가드레일) +8. [실전 사용 예시](#8-실전-사용-예시) + +--- + +## 1. 설정 파일 구조 + +``` +backend/ +├── CLAUDE.md # 프로젝트 규칙 (매 세션 자동 로드) +├── .claude/ +│ ├── settings.local.json # 권한 allowlist/denylist +│ ├── commands/ # 커스텀 슬래시 커맨드 (9종) +│ │ ├── analysis.md +│ │ ├── plan.md +│ │ ├── feat.md +│ │ ├── test.md +│ │ ├── check.md +│ │ ├── report.md +│ │ ├── perf.md +│ │ ├── commit.md +│ │ └── explain.md +│ └── agents/ # 전문가 서브에이전트 (7종) +│ ├── code-tracer.md +│ ├── permission-auditor.md +│ ├── perf-reviewer.md +│ ├── logic-reviewer.md +│ ├── db-safety.md +│ ├── test-writer.md +│ └── report-writer.md +└── docs/ + ├── plans/ # /plan 커맨드가 계획서를 저장하는 위치 + └── reports/ # /report 커맨드가 보고서를 저장하는 위치 +``` + +### 각 파일이 하는 일 + +| 파일 | 역할 | 로드 시점 | +|---|---|---| +| `CLAUDE.md` | 프로젝트 컨텍스트·가드레일·빌드 명령 | 매 세션 자동 | +| `commands/*.md` | `/커맨드명` 입력 시 실행할 프롬프트 템플릿 | 해당 커맨드 호출 시 | +| `agents/*.md` | 커맨드가 내부적으로 위임하는 전문 AI | 커맨드에서 호출 시 | +| `settings.local.json` | 승인 없이 실행 가능한 도구 범위 제한 | 매 세션 자동 | + +--- + +## 2. 커스텀 커맨드 9종 + +Claude Code에서 `/커맨드명 인자` 형식으로 호출합니다. + +### 사용 방법 + +``` +/커맨드명 # 인자 없이 (선택적 인자인 경우) +/커맨드명 작업 설명 # 자유 텍스트 인자 +/커맨드명 abc1234 # 커밋 해시 등 특정 값 +``` + +--- + +### `/analysis` — 원인 규명·분석 + +**목적**: 버그·이상 동작의 근본 원인을 코드에서 찾는다. 코드 수정 없음. + +``` +/analysis # 인자 없으면 3가지 질문을 먼저 물어봄 +/analysis 문제 등록 시 500 에러 발생 +``` + +**동작 흐름**: +1. 인자가 없으면 ① 증상 ② 원하는 결과 ③ 관련 경로·로그를 먼저 질문 +2. 가설 3~5개 나열 +3. `code-tracer` + 필요 시 검증 에이전트들을 **병렬**로 투입해 각 가설 독립 검증 +4. 에이전트 결과를 교차 반증 (모순·공백 식별) +5. 근본 원인 + 확신도 결론 + +**산출물**: 요약·근거(`파일:라인`)·근본 원인·다음 단계 제안 (파일 저장 없음) + +**읽기 전용** — 코드 수정·커밋 없음 + +--- + +### `/plan` — 구현 계획서 + +**목적**: 구현 전에 영향 범위·단계·위험 요소를 정리한다. 계획서를 파일로 저장. + +``` +/plan 문제 상세 조회 API에 태그 필터 추가 +/plan 복습 기록 이미지 업로드 S3 마이그레이션 +``` + +**동작 흐름**: +1. `code-tracer`로 관련 코드 조사 (필요 시 `permission-auditor`, `perf-reviewer`, `db-safety` 추가) +2. 계획서 작성 (목적 / 영향 범위 / 구현 단계 / 위험 요소 / 테스트 방향 / 커밋 분할 제안) +3. `docs/plans/<작업명>_plan.md`에 저장 +4. **승인 대기** — "검토 후 `/feat`으로 진행" 안내 + +**읽기 전용** — 계획서 파일만 저장, 코드 수정 없음 + +> 🔴 plan → feat 사이의 검토가 가장 중요한 체크포인트 + +--- + +### `/feat` — 기능 구현 + +**목적**: 승인된 계획서를 바탕으로 실제 코드를 구현한다. + +``` +/feat 태그 필터 추가 +/feat docs/plans/tag_filter_plan.md +``` + +**동작 흐름**: +1. `docs/plans/`에서 관련 계획서 탐색. 없으면 `code-tracer`로 파악 후 간략 계획 제시 → **승인 대기** +2. 계획 승인 전 구현 시작 금지 +3. 승인 후 구현. 엔티티 변경 시 `./gradlew compileJava` 자동 실행 +4. FCM·프로덕션 DB 파괴 작업 시 사전 경고 +5. `./gradlew compileJava`로 최종 컴파일 확인 +6. 논리 단위별 커밋 메시지 초안 제시 → 확인 후 커밋 + +--- + +### `/test` — 테스트 작성 + +**목적**: 변경된 코드에 대한 테스트를 작성하고 실행한다. + +``` +/test # 현재 변경분 대상 +/test ProblemService +/test com.aisip.OnO.backend.problem +``` + +**동작 흐름**: +1. 대상 도메인의 기존 테스트 품질 확인 → 좋으면 관례 따름, 아니면 새로 작성 +2. `test-writer` 에이전트 투입: + - test 프로필 + H2 격리 DB + - **사용자 격리 케이스 필수**: 본인 데이터 성공 / 타인 데이터 차단 / 인증 없음 401 + - 단위 테스트 + DB 연동 테스트 + - FCM·S3 등 외부 연동은 mock +3. `./gradlew test --tests "대상클래스"` 실행 → 실패 시 수정 + +--- + +### `/check` — 다관점 검증 + +**목적**: 구현된 코드를 4가지 관점에서 동시에 검증한다. 코드 수정 없음. + +``` +/check # 현재 git diff 대상 +/check ProblemController +/check docs/plans/tag_filter_plan.md # 특정 파일·계획서 대상 +``` + +**동작 흐름**: +1. 4개 에이전트를 **동시에** 투입: + - `permission-auditor`: userId 소유권 검증, 인증·인가 누락 + - `perf-reviewer`: N+1, JPA 풀스캔, 커넥션풀, 인덱스 + - `logic-reviewer`: 엣지/널, 동시성, 예외·롤백 + - `db-safety`: FCM 실발송, 잘못된 쓰기, 데이터 오염 +2. 결과 교차 검토 (에이전트 간 충돌·누락 재확인) +3. 위험도 분류 보고 + +**판정 기호**: +- 🔴 즉시 수정 필요 (사용자 영향, 보안, 데이터 오염) +- 🟠 배포 전 수정 권장 (성능, 예외 미처리) +- 🟡 다음 이터레이션 (마이너 개선) +- 🟢 이상 없음 + +**읽기 전용** — 코드 수정 없음 + +--- + +### `/report` — 결과 보고서 + +**목적**: 작업 완료 후 공유용 보고서를 작성한다. + +``` +/report abc1234 # 기준 커밋 해시부터 HEAD까지 +``` + +**동작 흐름**: +1. `code-tracer`로 `<커밋>..HEAD` 범위 변경 분석 +2. `report-writer` 에이전트로 보고서 작성 (존댓말, Before/After 표, 테스트 코드 내용 제외) +3. 파일명 제안 후 저장 위치 확인 → `docs/reports/<개발사항>_보고서.md` 저장 + +--- + +### `/perf` — 성능 병목 진단 + +**목적**: 특정 기능의 성능 문제를 진단하고 개선안 우선순위를 제시한다. + +``` +/perf 문제 목록 조회 API +/perf com.aisip.OnO.backend.problem.service +/perf 복습 기록 저장 흐름 +``` + +**동작 흐름**: +1. `perf-reviewer` + `code-tracer` **병렬** 투입 + - `perf-reviewer`: N+1, fetch 전략, 풀스캔, 커넥션풀, 배치 누락 + - `code-tracer`: 호출 경로 전체 추적, 쿼리 실행 지점 +2. 🔴/🟠/🟡 우선순위별 개선안 (문제 지점·원인·개선 방법·예상 효과) + +**읽기 전용** — 진단만, 수정은 `/plan` → `/feat`으로 + +--- + +### `/commit` — 커밋 단위 제안 + +**목적**: 현재 변경분을 논리 단위로 나누고 커밋 메시지 초안을 제시한다. 실제 커밋은 하지 않음. + +``` +/commit # 현재 변경 전체 분석 +/commit 태그 필터 + 성능 개선 함께 작업함 +``` + +**동작 흐름**: +1. `git diff HEAD` + `git status`로 변경 전체 파악 +2. 논리 단위 분할 (각 단위는 독립 빌드 가능, 단일 목적) +3. 단위별 커밋 메시지 제안 + 실행할 git 명령어 제시 + +**커밋 메시지 형식**: +``` +[Feat] 한 줄 요약 +- 상세 1 +- 상세 2 +``` +태그: `[Feat]` / `[Fix]` / `[Refactor]` / `[Chore]` / `[Test]` / `[Docs]` / `[Perf]` + +--- + +### `/explain` — 코드 흐름·원리 설명 + +**목적**: 특정 기능·클래스의 동작 원리와 설계 이유를 설명한다. + +``` +/explain 문제 등록 API +/explain ProblemService.createProblem +/explain JWT 인증 흐름 +``` + +**동작 흐름**: +1. `code-tracer`로 Controller → Service → Repository → 쿼리까지 전체 추적 +2. 요청 처리 순서 + 왜 이렇게 설계했는가 (`파일:라인` 포함) +3. 흐름이 복잡하면 텍스트 시퀀스 다이어그램 추가 + +**읽기 전용** — 코드 수정 없음. 언제든 호출 가능. + +--- + +## 3. 전문가 에이전트 7종 + +커맨드가 내부적으로 호출하는 전문 서브에이전트입니다. 직접 호출하지 않고 커맨드를 통해 사용합니다. + +| 에이전트 | 역할 | 도구 권한 | 호출 커맨드 | +|---|---|---|---| +| `code-tracer` | Controller→Service→Repository→쿼리 호출 경로 추적 | 읽기 전용 | analysis, plan, feat, explain, report, perf | +| `permission-auditor` | userId 소유권 검증·인증·인가 누락·필터 조건 검사 | 읽기 전용 | check, analysis | +| `perf-reviewer` | N+1·풀스캔·커넥션풀·배치 누락·인덱스 검사 | 읽기 전용 | check, perf, analysis | +| `logic-reviewer` | 엣지·널·동시성·예외·롤백·비즈 규칙 검사 | 읽기 전용 | check, analysis | +| `db-safety` | FCM 실발송·잘못된 쓰기·데이터 오염·S3 정합성 검사 | 읽기 전용 | check, analysis | +| `test-writer` | test 프로필+격리 DB, 사용자 격리 케이스 포함 테스트 작성 | 읽기+쓰기 | test | +| `report-writer` | 존댓말·Before/After 표·외부 공유용 보고서 작성 | 읽기+쓰기 | report | + +### 검증 에이전트 상세 + +#### `permission-auditor` — OnO 핵심 보안 에이전트 + +가장 중요한 에이전트. **사용자는 자신의 오답노트 데이터만 접근 가능**이라는 불변식을 검증한다. + +검사 항목: +- `findById()` 후 소유자 확인 없이 반환 (userId 검증 누락) +- `@AuthenticationPrincipal` 없이 데이터 반환 +- 관리자 전용 엔드포인트에 일반 사용자 접근 가능 +- 검색 조건 빈 값 → 전체 데이터 노출 +- ID 변조로 다른 사용자 데이터 접근 가능 + +#### `db-safety` — FCM 담당 에이전트 + +FCM은 실사용자에게 즉시 도달하며 되돌릴 수 없다. + +검사 항목: +- 개발/테스트 코드에서 실제 FCM 발송 경로를 타는 경우 +- 조건 없이 전체 사용자에게 발송 +- WHERE 조건 불충분한 UPDATE/DELETE +- S3 파일 삭제 후 DB 레코드 미삭제 (또는 반대) + +--- + +## 4. 커맨드 ↔ 에이전트 연결 구조 + +``` +/analysis ──┬── code-tracer (항상) + └── permission-auditor, perf-reviewer, logic-reviewer, db-safety (필요 시) + +/plan ───────┬── code-tracer (항상) + └── permission-auditor, perf-reviewer, db-safety (위험 시) + +/feat ───────── code-tracer (영향 범위 파악) + +/test ───────── test-writer + +/check ──────┬── permission-auditor ┐ + ├── perf-reviewer │ 4개 동시 병렬 + ├── logic-reviewer │ + └── db-safety ┘ + +/report ─────┬── code-tracer (변경 분석) + └── report-writer (보고서 작성) + +/perf ───────┬── perf-reviewer ┐ 2개 동시 병렬 + └── code-tracer ┘ + +/explain ────── code-tracer + +/commit ─── (에이전트 없음, 경량 명령) +``` + +--- + +## 5. 작업 파이프라인 + +### 일반 기능 개발 + +``` +analysis → plan → feat → test → check → commit → report + (선택) 🔴 🟠 │ 🟡 │ 🟢 + └── 통과 필수 ──┘ +``` + +- **analysis**: 요구가 명확하면 생략 가능 +- **plan** 후 🔴: 가장 중요한 검토 포인트. 사람이 직접 계획서 확인 후 진행 +- **feat** 후 🟠: 구현 완료 후 바로 test → check +- **check** 후 🟡: 🔴·🟠 항목은 반드시 수정 후 재확인 +- **report** 🟢: 완료 후 공유 필요 시 + +### 성능 개선 작업 + +``` +perf(진단) → plan → feat → test → check → commit → report +``` + +### 단발성 작업 + +``` +feat → check → commit +``` + +### 아무 때나 사용 + +- `/explain`: 코드 이해 필요할 때 언제든 +- `/commit`: 커밋 메시지 정리 필요할 때 + +--- + +## 6. 권한 설정 + +`.claude/settings.local.json`에서 도구 실행 권한을 제어합니다. + +### 자동 허용 (승인 팝업 없음) + +| 도구 | 이유 | +|---|---| +| `Read`, `Grep`, `Glob` | 읽기 전용, 부작용 없음 | +| `git status/diff/log/show/branch` | 조회만, 변경 없음 | +| `ls`, `find` | 파일 탐색, 변경 없음 | +| `./gradlew compileJava` | 컴파일 확인, 안전 | +| `./gradlew test --tests` | 테스트 실행, 안전 | +| `./gradlew build` | 전체 빌드 | + +### 차단 (실행 불가) + +| 도구 | 이유 | +|---|---| +| `git push --force` | 원격 히스토리 파괴 위험 | +| `git reset --hard` | 로컬 변경 손실 위험 | +| `git clean` | 추적 안 된 파일 삭제 위험 | + +### 승인 팝업이 뜨는 경우 + +allowlist에 없는 Bash 명령(실제 커밋, 파일 쓰기, 서버 시작 등)은 실행 전 팝업이 뜹니다. 승인하면 그 명령만 1회 실행됩니다. + +--- + +## 7. CLAUDE.md — 프로젝트 가드레일 + +`CLAUDE.md`는 세션 시작 시 자동으로 로드되어 모든 작업에 적용됩니다. + +### 핵심 가드레일 요약 + +**항상 지키는 것**: +- 라이브 서비스를 깨지 않는다 +- dev에서 검증 후 prod 반영 +- FCM 경로 수정·실행 전 사전 경고 +- 프로덕션 DB 파괴 작업은 명시적 확인 후 + +**자유롭게 하는 것**: +- git 커밋·push·브랜치·머지 +- Docker 배포·인프라 설정 + +**응답 방식**: +- 한국어 응답·주석·커밋 +- 근거는 `파일:라인` 형식 (추정 금지) +- 장시간 작업은 중간 진행 공유 + +--- + +## 8. 실전 사용 예시 + +### 예시 1: 신규 기능 개발 (문제에 태그 복수 추가) + +``` +# 1. 계획 먼저 +/plan 문제 등록 API에 태그 복수 추가 지원 + +# → docs/plans/태그복수추가_plan.md 생성 +# → 계획서 검토 후 승인 + +# 2. 구현 +/feat 문제 등록 태그 복수 추가 + +# → 컴파일 자동 확인 +# → 커밋 메시지 제안 + +# 3. 테스트 +/test ProblemService + +# → 사용자 격리 케이스 포함 테스트 작성 + 실행 + +# 4. 검증 +/check + +# → 4개 에이전트 병렬 검증 +# → 🔴🟠 항목 있으면 수정 후 재확인 + +# 5. 커밋 +/commit +``` + +### 예시 2: 버그 조사 + +``` +# 원인 모를 때 +/analysis 다른 사용자 문제가 조회되는 버그 + +# → 에이전트 병렬 조사 → 근본 원인 + 파일:라인 +``` + +### 예시 3: 성능 개선 + +``` +# 병목 진단 +/perf 복습 기록 목록 조회 + +# → N+1 등 발견 → /plan으로 개선 계획 수립 +``` + +### 예시 4: 코드 이해 + +``` +# 언제든 +/explain JWT 인증 흐름 +/explain ProblemController.getProblemList +``` + +### 예시 5: 작업 보고서 + +``` +# 기준 커밋 이후 변경사항 보고서 +/report 7edc25a + +# → docs/reports/문제등록개선_보고서.md 생성 +``` + +--- + +## 자주 묻는 것 + +**Q: `/feat` 없이 직접 구현 요청해도 되나?** +A: 됩니다. 다만 Claude는 계획 제시 후 승인을 기다립니다. 빠르게 가려면 `/plan` 먼저 해두면 더 매끄럽습니다. + +**Q: `/check`는 언제 쓰나?** +A: `/feat` 완료 직후 습관적으로. 특히 권한·FCM·쓰기 로직이 포함된 경우 필수입니다. + +**Q: 에이전트를 직접 호출할 수 있나?** +A: 기술적으로는 가능하지만, 커맨드를 통해 호출하는 것이 권장 방식입니다. 커맨드가 올바른 컨텍스트와 제약을 함께 전달합니다. + +**Q: `docs/plans/`에 계획서가 쌓이면 어떻게 하나?** +A: 완료된 작업의 계획서는 삭제하거나 `docs/plans/archive/`로 이동해도 됩니다. diff --git a/.claude/commands/analysis.md b/.claude/commands/analysis.md new file mode 100644 index 00000000..934523a2 --- /dev/null +++ b/.claude/commands/analysis.md @@ -0,0 +1,32 @@ +--- +description: 문제 원인 규명·분석 (읽기 전용). 코드 수정·커밋 금지. +argument-hint: "[분석할 증상이나 주제 — 없으면 3가지 질문을 먼저 물어봄]" +--- + +# /analysis — 원인 규명·분석 + +**읽기 전용 명령: 코드 수정·커밋 절대 금지.** + +## 시작 + +인자($ARGUMENTS)가 없거나 불명확하면, 먼저 아래 3가지를 물어보고 답을 받은 뒤 진행: +1. 무슨 문제가 발생했나? (증상·에러·예상과 다른 동작) +2. 원하는 결과는 무엇인가? +3. 관련 파일·경로·로그가 있으면 알려달라. + +## 조사 단계 + +1. **조사각도 분해** — 이 문제를 설명할 수 있는 가설 3~5개를 먼저 나열한다. +2. **병렬 조사** — `code-tracer` 에이전트와 필요 시 `permission-auditor`, `perf-reviewer`, `logic-reviewer`, `db-safety` 에이전트를 병렬로 투입해 각 가설을 독립적으로 검증한다. + - 각 에이전트는 Read, Grep, Glob, Bash(git log/diff/show) 도구만 사용. + - 근거는 반드시 `파일:라인` 형식으로 명시. 추정 금지. +3. **교차 반증** — 각 에이전트 결과를 서로 교차 검증: 가설 A를 지지하는 증거가 가설 B를 반박하는가? 모순·공백 식별. +4. **종합** — 확신도(높음/중간/낮음)와 함께 근본 원인 결론 도출. + +## 산출물 + +분석 보고서 (텍스트, 저장 불필요): +- **요약**: 한 문장 결론 +- **근거**: 파일:라인 목록 +- **근본 원인 + 확신도** +- **다음 단계 제안** (plan 또는 추가 조사 방향) \ No newline at end of file diff --git a/.claude/commands/check.md b/.claude/commands/check.md new file mode 100644 index 00000000..28a5f8ab --- /dev/null +++ b/.claude/commands/check.md @@ -0,0 +1,32 @@ +--- +description: 다관점 검증 (읽기 전용). 코드 수정·커밋 금지. +argument-hint: "[검증 대상 — 미지정 시 현재 git diff]" +--- + +# /check — 다관점 검증 + +**읽기 전용 명령: 코드 수정·커밋 절대 금지.** + +## 대상: $ARGUMENTS (미지정 시 현재 git diff) + +## 단계 + +1. **전문가 병렬 검증** — 아래 에이전트를 동시에 투입: + - `permission-auditor`: 사용자별 오답노트 데이터 격리, 인증·인가 누락 검증 + - `perf-reviewer`: N+1, JPA 풀스캔, 커넥션풀, 인덱스 누락 검증 + - `logic-reviewer`: 엣지/널, 동시성, 예외·롤백, 비즈 로직 정합성 검증 + - `db-safety`: FCM 실발송 경로, 잘못된 MySQL 쓰기, 데이터 오염 검증 + - **마이그레이션 파일이 포함되면**: `migration-safety` 추가 투입 + - **DTO·API 엔드포인트 변경이 포함되면**: `api-contract-checker` 추가 투입 + +2. **2차 반증** — 각 에이전트 결과를 교차 검토: + - A가 발견한 문제를 B가 놓쳤는가? + - 에이전트 간 충돌하는 판단이 있는가? → 재검토. + +3. **위험도 분류 보고**: + - 🔴 **즉시 수정 필요** (사용자 영향, 데이터 오염, 보안) + - 🟠 **배포 전 수정 권장** (성능 저하, 예외 미처리) + - 🟡 **다음 이터레이션** (마이너 개선) + - 🟢 **이상 없음** + +근거는 반드시 `파일:라인`. 추정 금지. diff --git a/.claude/commands/commit.md b/.claude/commands/commit.md new file mode 100644 index 00000000..aa42997b --- /dev/null +++ b/.claude/commands/commit.md @@ -0,0 +1,28 @@ +--- +description: 현재 변경분을 논리 단위로 분할하고 커밋 메시지 제안 (실제 커밋 X). +argument-hint: "[추가 맥락 — 선택]" +--- + +# /commit — 커밋 단위 제안 + +**실제 커밋 실행 안 함. 제안만.** + +## 맥락: $ARGUMENTS + +## 단계 + +1. `git diff HEAD`와 `git status`로 현재 변경 전체 파악. + +2. 변경을 논리 단위로 분할: + - 각 단위는 독립적으로 빌드 가능하고 단일 목적. + - 관련 없는 변경이 섞이지 않도록. + +3. 각 단위별 커밋 메시지 제안: + ``` + [태그] 한 줄 요약 + - 상세 1 + - 상세 2 + ``` + 태그: `[Feat]` / `[Fix]` / `[Refactor]` / `[Chore]` / `[Test]` / `[Docs]` / `[Perf]` + +4. 제안된 순서대로 커밋하는 명령어도 함께 제시. diff --git a/.claude/commands/explain.md b/.claude/commands/explain.md new file mode 100644 index 00000000..7ce1eb0d --- /dev/null +++ b/.claude/commands/explain.md @@ -0,0 +1,23 @@ +--- +description: 코드 흐름·원리 설명 (읽기 전용, 학습용). 코드 수정·커밋 금지. +argument-hint: "<설명할 기능·클래스·엔드포인트>" +--- + +# /explain — 코드 흐름·원리 설명 + +**읽기 전용 명령: 코드 수정·커밋 절대 금지.** + +## 대상: $ARGUMENTS + +## 단계 + +1. **`code-tracer` 에이전트** — 대상의 진입점부터 전체 호출 경로 추적: + - Controller → Service → Repository → 쿼리 레이어 + - 데이터 흐름, 변환 지점, 분기 조건 + +2. **흐름 설명**: + - 요청이 어떻게 처리되는가 (순서대로) + - **왜 이렇게 설계했는가** (도메인 이유, 성능 이유, 제약 조건) + - 주요 클래스·메서드의 역할 (`파일:라인` 포함) + +3. **다이어그램** (선택) — 흐름이 복잡하면 텍스트 시퀀스 다이어그램 추가. diff --git a/.claude/commands/feat.md b/.claude/commands/feat.md new file mode 100644 index 00000000..34333bf0 --- /dev/null +++ b/.claude/commands/feat.md @@ -0,0 +1,28 @@ +--- +description: 기능 구현. 계획 승인 후 실행. +argument-hint: "<작업 설명 또는 계획서 경로>" +--- + +# /feat — 기능 구현 + +## 작업: $ARGUMENTS + +## 단계 + +1. **조사 & 계획 확인** — 관련 계획서(`docs/plans/`)가 있으면 읽는다. 없으면 `code-tracer` 에이전트로 영향 범위 파악 후 간략 계획을 제시하고 **승인 대기**. + +2. **승인 확인** — 계획이 승인되지 않았다면 구현 시작 금지. "계획을 확인해주세요." 안내. + +3. **구현** — 승인된 계획에 따라 구현: + - 엔티티 변경 시 `./gradlew compileJava`로 컴파일 확인. + - 가드레일 준수: FCM 경로·프로덕션 DB 파괴 작업 전 경고. + - 사용자 격리(userId 소유권 검증) 누락 없이 구현. + - 근거 없는 코드 추가 금지. 각 변경은 계획서 단계에 대응. + +4. **컴파일 확인** — 구현 완료 후 `./gradlew compileJava` 실행. 오류 있으면 수정. + +5. **커밋 제안** — 논리 단위별 커밋 메시지 초안을 제시하고 사용자 확인 후 커밋. + 형식: `[Feat] 한 줄 요약\n- 상세` + (테스트는 /test로 분리) + +> 🟠 구현 완료 후 /test → /check 순서로 검증 권장. diff --git a/.claude/commands/migrate.md b/.claude/commands/migrate.md new file mode 100644 index 00000000..90925ec0 --- /dev/null +++ b/.claude/commands/migrate.md @@ -0,0 +1,34 @@ +--- +description: Flyway 마이그레이션 파일 작성 + 안전성 검토. DB 스키마 변경이 필요할 때 사용. +argument-hint: "<스키마 변경 내용 설명>" +--- + +# /migrate — DB 마이그레이션 + +## 작업: $ARGUMENTS + +## 단계 + +1. **기존 마이그레이션 파악** — `src/main/resources/db/migration/`에서 최신 버전 번호 확인. + `code-tracer`로 변경할 테이블의 현재 엔티티·쿼리 구조 파악. + +2. **마이그레이션 초안 작성** — 다음 번호(`V{N+1}__<설명>.sql`)로 SQL 초안 작성: + - 안전한 순서: `ADD COLUMN NULL` → 백필 UPDATE → `ALTER COLUMN NOT NULL` (한 파일 또는 단계별 분리) + - 대용량 테이블이면 락 영향 명시 + - 롤백 SQL 주석으로 포함 + +3. **`migration-safety` 에이전트로 검토** — 초안을 검토: + - 되돌리기 불가 작업 여부 + - 락 위험 + - 데이터 정합성 + - Flyway 파일 형식 + +4. **API 변경 수반 시** — DTO·엔드포인트도 변경된다면 `api-contract-checker` 에이전트 추가 투입: + - Flutter 앱에 breaking change 여부 판단 + +5. **검토 결과 보고 + 승인 대기** — 🔴·🟠 항목이 있으면 수정 후 재검토. + 안전 확인 후: "마이그레이션 파일을 저장할까요?" 확인. + +6. **파일 저장** — 승인 시 `V{N+1}__<설명>.sql` 파일 생성. + +> ⚠️ dev에서 먼저 적용 테스트 후 prod 반영. 프로덕션 Flyway는 자동 실행되므로 한 번 머지되면 되돌리기 어렵다. diff --git a/.claude/commands/perf.md b/.claude/commands/perf.md new file mode 100644 index 00000000..a81faf6e --- /dev/null +++ b/.claude/commands/perf.md @@ -0,0 +1,25 @@ +--- +description: 성능 병목 진단 (읽기 전용). 코드 수정·커밋 금지. +argument-hint: "<진단 대상 기능·엔드포인트·패키지>" +--- + +# /perf — 성능 병목 진단 + +**읽기 전용 명령: 코드 수정·커밋 절대 금지.** + +## 대상: $ARGUMENTS + +## 단계 + +1. **병렬 점검** — `perf-reviewer`와 `code-tracer` 에이전트를 동시에 투입: + - `perf-reviewer`: N+1 쿼리, JPA fetch 전략, 풀스캔, 커넥션풀 고갈, 데드락, 인덱스 누락, 페이지네이션 없는 대용량 조회 + - `code-tracer`: 대상 기능의 전체 호출 경로 추적, 쿼리 실행 지점 식별 + +2. **개선안 우선순위**: + - 🔴 즉시 개선 (사용자 체감 레이턴시·OOM 위험) + - 🟠 배포 전 개선 권장 + - 🟡 백로그 + +3. **각 개선안마다**: 문제 지점(`파일:라인`), 원인, 구체적 개선 방법, 예상 효과. + +> 진단 완료 후 /plan으로 개선 계획 수립 권장. diff --git a/.claude/commands/plan.md b/.claude/commands/plan.md new file mode 100644 index 00000000..73b435c7 --- /dev/null +++ b/.claude/commands/plan.md @@ -0,0 +1,30 @@ +--- +description: 구현 계획서 작성 (구현 X). 계획서 파일을 저장한다. +argument-hint: "<작업 설명>" +--- + +# /plan — 구현 계획서 + +**읽기 전용 명령: 코드 수정·커밋 절대 금지. 계획서 파일만 저장.** + +## 작업: $ARGUMENTS + +## 단계 + +1. **조사** — `code-tracer` 에이전트로 관련 코드 흐름 파악. 필요 시 `permission-auditor`, `perf-reviewer`, `db-safety` 에이전트 추가 투입. 근거는 `파일:라인`. + - **DB 스키마 변경이 포함되면**: `migration-safety` 에이전트로 마이그레이션 안전성 사전 검토. + - **DTO·엔드포인트 변경이 포함되면**: `api-contract-checker` 에이전트로 Flutter 앱 breaking change 여부 확인. + +2. **계획서 작성** — 아래 섹션을 포함: + - **목적**: 한 줄 요약 + - **영향 범위**: 수정될 파일·패키지·레이어 목록 + - **구현 단계**: 번호 있는 순서 (각 단계 독립적으로 구현 가능하게) + - **위험 요소**: FCM 발송·데이터 마이그레이션·권한 변경 등 주의 지점 + - **테스트 방향**: 어떤 경우를 검증해야 하는가 (사용자 격리 포함) + - **커밋 분할 제안**: 논리 단위별 커밋 메시지 초안 + +3. **저장** — 계획서를 `docs/plans/<작업명>_plan.md`에 저장. 저장 경로를 사용자에게 알린다. + +4. **승인 대기** — "계획서를 검토해주세요. 승인하면 /feat으로 진행합니다." 라고 안내. + +> 🔴 계획서 검토는 가장 중요한 체크포인트다. 사용자 확인 전 구현 시작 금지. diff --git a/.claude/commands/report.md b/.claude/commands/report.md new file mode 100644 index 00000000..60940cba --- /dev/null +++ b/.claude/commands/report.md @@ -0,0 +1,22 @@ +--- +description: 작업 결과 보고서 작성 및 저장. +argument-hint: "<기준 커밋 해시>" +--- + +# /report — 결과 보고서 + +## 기준: $ARGUMENTS (커밋 해시) + +## 단계 + +1. **변경 분석** — `code-tracer` 에이전트로 `<커밋>..HEAD` 범위의 변경 내용 분석: + - 수정된 파일·레이어 목록 + - 각 변경의 목적과 영향 + - Before/After 비교 가능한 핵심 지점 + +2. **`report-writer` 에이전트로 보고서 작성**: + - 존댓말, 핵심 위주, Before/After 표 포함 + - 테스트 코드 내용 제외 (결과만) + - 외부에 공유 가능한 수준으로 + +3. **저장 위치 확인** — 보고서 파일명(`<개발사항>_보고서.md`)을 제안하고 저장 위치를 사용자에게 물어본 뒤 저장. diff --git a/.claude/commands/test.md b/.claude/commands/test.md new file mode 100644 index 00000000..bc378f12 --- /dev/null +++ b/.claude/commands/test.md @@ -0,0 +1,25 @@ +--- +description: 테스트 작성 및 실행. +argument-hint: "[테스트 대상 — 미지정 시 현재 변경분]" +--- + +# /test — 테스트 작성 + +## 대상: $ARGUMENTS (미지정 시 현재 git diff 변경분) + +## 단계 + +1. **기존 테스트 품질 확인** — 대상 도메인의 기존 테스트를 먼저 읽는다. + 좋으면 관례를 따르고, 그렇지 않으면 새로 깔끔하게 작성. + +2. **`test-writer` 에이전트 투입** — 아래 원칙으로 테스트 작성: + - test 프로필 + 격리 DB(H2 또는 전용 test DB) 사용. + - **사용자 격리 필수**: 다른 사용자의 데이터에 접근할 수 없음을 반드시 검증. + - 권한·필터·검색 조건의 모든 경우(정상/경계/실패) 망라. + - 기존 테스트 복붙 금지 — 독립적으로 작성. + - 단위 테스트 + DB 연동 테스트 위주. 외부 연동(FCM 등)은 mock. + +3. **해당 테스트만 실행** — `./gradlew test --tests "대상클래스"` 실행 후 결과 확인. + 실패 시 원인 파악 및 수정. + +4. **통과 확인** — 모든 작성 테스트가 통과하면 결과 보고. diff --git a/.claude/hooks/db_safety_check.py b/.claude/hooks/db_safety_check.py new file mode 100644 index 00000000..5eb6976e --- /dev/null +++ b/.claude/hooks/db_safety_check.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +""" +PreToolUse hook: Bash 명령에서 위험한 DB 작업 패턴 감지 시 차단. +Claude가 DROP/TRUNCATE/무조건 DELETE 등을 실행하려 할 때 경고 후 중단. +""" +import sys +import json +import re + +try: + data = json.load(sys.stdin) +except Exception: + sys.exit(0) +cmd = data.get("tool_input", {}).get("command", "") + +DANGEROUS = [ + (r"DROP\s+TABLE", "DROP TABLE"), + (r"TRUNCATE\s+(TABLE\s+)?[`\w][`\w.]*", "TRUNCATE"), + (r"ALTER\s+TABLE\s+[`\w][`\w.]*\s+DROP", "DROP COLUMN/INDEX"), + (r"DROP\s+DATABASE", "DROP DATABASE"), +] + +found = [label for pattern, label in DANGEROUS if re.search(pattern, cmd, re.IGNORECASE)] + +# WHERE 없는 DELETE — 백틱·스키마 프리픽스 포함, WHERE 절 있으면 제외 +if (re.search(r"DELETE\s+FROM", cmd, re.IGNORECASE) + and not re.search(r"\bWHERE\b", cmd, re.IGNORECASE)): + found.append("WHERE 없는 DELETE") + +if found: + print(f"🚨 위험한 DB 작업 감지: {', '.join(found)}") + print(f" 명령: {cmd[:200]}") + print(" 프로덕션 영향 범위를 확인한 뒤 명시적으로 허용해 주세요.") + sys.exit(2) \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..81377742 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,67 @@ +# OnO 백엔드 — CLAUDE.md + +## Operating constraints + +### 자유롭게 해도 되는 것 +- git 작업 자유: 커밋·push·브랜치·머지 등 (소유자 판단). +- 인프라 작업 자유: Docker 배포, 설정 변경, 인프라 리소스. + +### 실사용자 서비스 — 프로덕션 안전 우선 +- **라이브 서비스를 깨지 말 것.** 되돌리기 어렵거나 사용자에게 영향 가는 변경(배포·스키마 변경·데이터 마이그레이션)은 실행 전 영향 범위를 설명하고 확인받는다. +- **dev 서버에서 먼저 검증 후 prod 반영.** 프로덕션 MySQL 파괴·마이그레이션 신중. +- **FCM = 실사용자 푸시 발송.** 발송 경로를 수정하거나 실행하기 전 반드시 경고하고 확인받는다. +- 프로덕션 DB에 대한 삭제/대량 수정/truncate는 명시적 확인 없이 실행하지 않는다. +- 되돌리기 어려운/외부로 나가는 작업은 한 번 확인. + +### 작업 방식 +- 커밋 메시지 형식: + ``` + [Feat] 한 줄 요약 + - 상세 1 + - 상세 2 + ``` + 태그: `[Feat]` / `[Fix]` / `[Refactor]` / `[Chore]` / `[Test]` / `[Docs]` / `[Perf]` +- 장시간 작업은 중간 진행 공유. +- **응답 언어: 한국어.** 주석·커밋도 한국어. +- 근거는 반드시 `파일:라인` 형식으로 명시. 추정 금지. + +## 빌드 / 테스트 +```bash +./gradlew compileJava # 엔티티 변경 후 컴파일 확인 +./gradlew test --tests "패키지.클래스.메서드" # 특정 테스트 실행 +./gradlew build # 전체 빌드 +``` + +## 프로젝트 구조 + +**스택**: Spring Boot + MySQL + Docker, dev/prod 환경 분리 +**루트 패키지**: `com.aisip.OnO.backend` + +| 패키지 | 도메인 | +|---|---| +| `auth` | 인증 (OAuth, JWT) | +| `user` | 사용자 계정·프로필 | +| `problem` | 오답 문제 등록·조회·수정·삭제 | +| `practicenote` | 복습 기록 | +| `folder` | 폴더 (문제 분류) | +| `tag` | 태그 | +| `mission` | 미션·보상 | +| `problemsolve` | 문제 풀이 기록 | +| `learningcalendar` | 학습 캘린더 | +| `learningreport` | 학습 리포트 | +| `admin` | 관리자 기능 | +| `common` | 공통 유틸·예외·응답 | +| `config` | 설정 (Security, JPA 등) | +| `util` | 유틸리티 | + +**핵심 불변식**: 모든 데이터 접근은 `userId` 기준 소유권 검증 필수 — 사용자는 자신의 오답노트 데이터만 접근 가능. + +## 작업 파이프라인 + +``` +일반 기능: analysis → plan → feat → test → check → commit → report +성능 작업: perf(진단) → plan → feat → test → check → commit → report +``` +- 단계 경계에서 사람이 검토 후 다음으로 진행 (자동 연쇄 X). +- `analysis`는 요구 명확하면 생략 가능. `explain`은 아무 때나. +- 검토 강도: plan 후 🔴최고 / feat 후 🟠 / check 결과 🟡 / report 🟢. \ No newline at end of file From ee9d49faba9baf5cc1ee2d55016be0d1fb5829db Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 4 Jun 2026 22:51:43 +0900 Subject: [PATCH 03/67] =?UTF-8?q?[Chore]=20Codex=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - build.config.toml, review.config.toml: 빌드·리뷰 모드 설정 - skills/: feat, plan, check, test, perf 등 Codex 스킬 정의 - scripts/codex-notify-macos.sh: macOS 알림 스크립트 --- .codex/USAGE.md | 472 +++++++++++++++++++ .codex/build.config.toml | 3 + .codex/review.config.toml | 3 + .codex/scripts/codex-notify-macos.sh | 17 + .codex/skills/analysis/SKILL.md | 28 ++ .codex/skills/analysis/agents/openai.yaml | 7 + .codex/skills/check/SKILL.md | 44 ++ .codex/skills/check/agents/openai.yaml | 7 + .codex/skills/commit/SKILL.md | 33 ++ .codex/skills/commit/agents/openai.yaml | 7 + .codex/skills/explain/SKILL.md | 20 + .codex/skills/explain/agents/openai.yaml | 7 + .codex/skills/feat/SKILL.md | 31 ++ .codex/skills/feat/agents/openai.yaml | 7 + .codex/skills/perf-audit/SKILL.md | 28 ++ .codex/skills/perf-audit/agents/openai.yaml | 7 + .codex/skills/perf/SKILL.md | 22 + .codex/skills/perf/agents/openai.yaml | 7 + .codex/skills/plan/SKILL.md | 29 ++ .codex/skills/plan/agents/openai.yaml | 7 + .codex/skills/report/SKILL.md | 24 + .codex/skills/report/agents/openai.yaml | 7 + .codex/skills/test-writer/SKILL.md | 23 + .codex/skills/test-writer/agents/openai.yaml | 7 + .codex/skills/test/SKILL.md | 21 + .codex/skills/test/agents/openai.yaml | 7 + 26 files changed, 875 insertions(+) create mode 100644 .codex/USAGE.md create mode 100644 .codex/build.config.toml create mode 100644 .codex/review.config.toml create mode 100755 .codex/scripts/codex-notify-macos.sh create mode 100644 .codex/skills/analysis/SKILL.md create mode 100644 .codex/skills/analysis/agents/openai.yaml create mode 100644 .codex/skills/check/SKILL.md create mode 100644 .codex/skills/check/agents/openai.yaml create mode 100644 .codex/skills/commit/SKILL.md create mode 100644 .codex/skills/commit/agents/openai.yaml create mode 100644 .codex/skills/explain/SKILL.md create mode 100644 .codex/skills/explain/agents/openai.yaml create mode 100644 .codex/skills/feat/SKILL.md create mode 100644 .codex/skills/feat/agents/openai.yaml create mode 100644 .codex/skills/perf-audit/SKILL.md create mode 100644 .codex/skills/perf-audit/agents/openai.yaml create mode 100644 .codex/skills/perf/SKILL.md create mode 100644 .codex/skills/perf/agents/openai.yaml create mode 100644 .codex/skills/plan/SKILL.md create mode 100644 .codex/skills/plan/agents/openai.yaml create mode 100644 .codex/skills/report/SKILL.md create mode 100644 .codex/skills/report/agents/openai.yaml create mode 100644 .codex/skills/test-writer/SKILL.md create mode 100644 .codex/skills/test-writer/agents/openai.yaml create mode 100644 .codex/skills/test/SKILL.md create mode 100644 .codex/skills/test/agents/openai.yaml diff --git a/.codex/USAGE.md b/.codex/USAGE.md new file mode 100644 index 00000000..1d031a30 --- /dev/null +++ b/.codex/USAGE.md @@ -0,0 +1,472 @@ +# OnO Codex 사용법 + +이 폴더는 OnO 백엔드 레포에서 Codex를 일관된 방식으로 쓰기 위한 설정과 스킬을 담는다. + +## 1. 설치된 구성 + +```text +.codex/ +├── review.config.toml +├── build.config.toml +├── scripts/codex-notify-macos.sh +└── skills/ + ├── analysis/ + ├── check/ + ├── commit/ + ├── feat/ + ├── plan/ + ├── report/ + ├── test/ + ├── test-writer/ + ├── perf/ + ├── perf-audit/ + └── explain/ +``` + +실제 Codex CLI가 바로 읽는 위치는 `~/.codex`다. 현재 이 레포의 `.codex` 내용은 다음 위치에도 설치되어 있다. + +```text +~/.codex/review.config.toml +~/.codex/build.config.toml +~/.codex/skills/analysis +~/.codex/skills/check +~/.codex/skills/commit +~/.codex/skills/feat +~/.codex/skills/plan +~/.codex/skills/report +~/.codex/skills/test +~/.codex/skills/test-writer +~/.codex/skills/perf +~/.codex/skills/perf-audit +~/.codex/skills/explain +~/.codex/scripts/codex-notify-macos.sh +``` + +## 2. 프로파일 사용 + +현재 로컬 Codex CLI 기준으로 별도 config 파일은 `--profile-v2`로 사용한다. + +### 분석/검토 모드 + +코드를 수정하지 않고 계획, 검토, 설명, 위험 분석을 할 때 사용한다. + +```bash +codex --profile-v2 review +``` + +설정 내용: + +```toml +model_reasoning_effort = "xhigh" +sandbox_mode = "read-only" +approval_policy = "on-request" +``` + +사용 예: + +```bash +codex --profile-v2 review +``` + +그 다음 Codex 안에서: + +```text +check 현재 변경사항 위험 검토해줘 +``` + +또는 빌트인 리뷰: + +```text +/review +``` + +### 구현/테스트 모드 + +파일 수정, 테스트 작성, 빌드 검증이 필요한 작업에 사용한다. + +```bash +codex --profile-v2 build +``` + +설정 내용: + +```toml +model_reasoning_effort = "high" +sandbox_mode = "workspace-write" +approval_policy = "on-request" +``` + +사용 예: + +```bash +codex --profile-v2 build +``` + +그 다음 Codex 안에서: + +```text +feat docs/...의 구현 명세서를 기준으로 기능 구현해줘 +``` + +## 3. 커스텀 명령어 스킬 사용법 + +스킬은 명시적으로 `$skill-name` 형태로 호출할 수 있고, 요청 내용이 description과 맞으면 자동으로 호출될 수 있다. + +현재 `.claude/commands`와 맞춰 제공하는 주요 명령어 이름: + +```text +analysis check commit explain feat perf plan report test +``` + +보조 스킬: + +```text +test-writer perf-audit +``` + +공통 규칙: + +- 커스텀 명령어에 인자를 넣지 않으면 기본적으로 `git status`와 `git diff HEAD` 기준의 현재 변경사항을 대상으로 수행한다. +- `analysis`, `check`, `feat`는 완료 직전에 반드시 아래 자가 반증 질문에 답한 뒤 결론 또는 구현 결과를 확정한다. + +```text +내가 놓쳤을 가능성이 큰 것은? +이 변경이 실제 사용자에게 깨질 수 있는 경로는? +이 결론이 틀렸다면 어떤 조건 때문인가? +``` + +### analysis + +문제 원인을 읽기 전용으로 분석할 때 사용한다. + +```text +$analysis 소셜 로그인 후 특정 사용자만 401이 나는 원인 분석해줘 +``` + +인자를 생략하면 현재 변경사항을 대상으로 분석한다. + +```text +$analysis +``` + +수행 방식: + +- 가능한 원인 가설을 3~5개로 나눈다. +- 관련 코드, 설정, 로그, git diff/log를 읽기 전용으로 확인한다. +- 각 가설을 `파일:라인` 근거로 지지하거나 반박한다. +- 완료 직전에 자가 반증 3문항에 답하고 결론을 수정할지 판단한다. +- 근본 원인, 확신도, 다음 단계를 보고한다. + +### check + +현재 변경사항이나 특정 커밋 이후 변경을 다관점으로 검토할 때 사용한다. + +```text +$check 현재 변경사항 위험 검토해줘 +``` + +인자를 생략해도 현재 변경사항을 대상으로 검토한다. + +```text +$check +``` + +또는: + +```text +$check abc1234 이후 변경사항 검토해줘 +``` + +수행 방식: + +- `git status`, `git diff`, 필요 시 `git log/show`로 변경 범위를 좁힌다. +- 사용자 데이터 격리, 권한, N+1, 풀스캔, 트랜잭션, FCM/메일 실발송, DB schema 안전성을 확인한다. +- 1차 결론을 다시 반증해 실제 도달 가능한 문제인지 확인한다. +- 완료 직전에 자가 반증 3문항에 답하고 위험도 판단을 수정할지 확인한다. +- 위험도 `🔴/🟠/🟡/🟢` 순서로 보고한다. + +### commit + +현재 변경사항의 커밋 단위와 메시지만 제안할 때 사용한다. 실제 커밋은 명시 요청 전까지 실행하지 않는다. + +```text +$commit 현재 변경사항 커밋 단위 추천해줘 +``` + +수행 방식: + +- `git status`, `git diff HEAD`로 변경 전체를 파악한다. +- 서로 독립적인 논리 단위로 나눈다. +- OnO 커밋 메시지 형식으로 제안한다. + +```text +[Feat] 구현 타이틀 - 구현 상세1 구현 상세2 +``` + +### feat + +승인된 계획서나 명확한 작업 설명을 기준으로 기능을 구현할 때 사용한다. + +```text +$feat docs/<기능폴더>/<명세서>.md 기준으로 구현해줘 +``` + +인자를 생략하면 현재 변경사항을 읽고 남은 구현, 정리, 검증 대상을 파악한다. + +```text +$feat +``` + +수행 방식: + +- 기준 구현 명세서를 먼저 확인한다. +- 명세서가 불명확하면 어떤 문서를 기준으로 할지 묻는다. +- API 계약, DTO, validation, 예외 처리, transaction, 권한, 성능, 데이터 정합성을 확인하며 구현한다. +- 가능한 범위에서 테스트, 빌드, 정적 검사를 실행한다. +- 완료 직전에 자가 반증 3문항에 답하고 추가 수정 또는 리스크 보고가 필요한지 확인한다. +- 완료 후 사용자 영향, 검증 결과, 남은 리스크, 추천 커밋 메시지를 보고한다. + +### plan + +구현 전 명세서를 작성할 때 사용한다. 코드는 수정하지 않고 `docs` 아래에 계획서를 만든다. + +```text +$plan 학습 기록 검색 API 필터 기능 구현 계획 작성해줘 +``` + +또는: + +```text +plan 학습 기록 검색 API 필터 기능 +``` + +수행 방식: + +- 관련 코드와 문서를 먼저 확인한다. +- API 계약, 데이터 흐름, 에러 처리, 테스트 방법, 배포 리스크를 정리한다. +- `/Users/ksm/programing/sw_maestro/OnO_BACKEND/backend/docs` 아래 기능별 폴더에 markdown을 작성한다. + +### test-writer + +테스트 구현 원칙을 더 강하게 적용할 때 사용하는 보조 스킬이다. + +```text +$test-writer 이번 변경에 대한 API 통합 테스트 추가해줘 +``` + +수행 방식: + +- 기존 테스트 구조와 test profile을 먼저 확인한다. +- validation 실패, 권한 실패, null/empty 입력, DB 반영을 위험도에 맞춰 검증한다. +- MySQL/PostgreSQL 환경에 `ddl-auto=create` 또는 `create-drop`을 추가하지 않는다. + +### test + +`.claude/commands/test.md`에 대응하는 테스트 작성/실행 명령이다. + +```text +$test 현재 변경사항에 필요한 테스트 작성하고 실행해줘 +``` + +수행 방식: + +- 대상이 없으면 현재 git diff 기준으로 테스트 대상을 정한다. +- 기존 테스트 구조와 test profile을 확인한다. +- 사용자 격리, 권한 실패, validation 실패, null/empty, 예외 케이스를 위험도에 맞춰 포함한다. +- 관련 테스트만 우선 실행하고 결과를 보고한다. + +### report + +특정 커밋부터 현재까지의 작업 보고서나 PR 초안을 작성할 때 사용한다. + +```text +$report abc1234 이후 변경사항 보고서 작성해줘 +``` + +수행 방식: + +- `git log`, `git diff`, `git status`로 커밋된 변경과 미커밋 변경을 구분한다. +- 변경사항 요약, 사용자 영향, 검증 결과, 배포 리스크를 정리한다. +- 저장 위치가 불명확하면 먼저 확인한다. + +### perf + +`.claude/commands/perf.md`에 대응하는 성능 병목 진단 명령이다. + +```text +$perf 오답노트 목록 조회 API 성능 병목 진단해줘 +``` + +수행 방식: + +- 대상 기능의 호출 경로와 쿼리 실행 지점을 추적한다. +- N+1, fetch 전략, 풀스캔, pagination 누락, index 부재, 커넥션 풀 고갈 가능성을 확인한다. +- `🔴/🟠/🟡` 우선순위로 개선안을 보고한다. + +### perf-audit + +성능 문제를 더 넓게 점검하는 보조 스킬이다. `perf`와 같은 목적이지만 Flutter rebuild/jank까지 명시적으로 포함한다. + +```text +$perf-audit 오답노트 목록 조회 API가 느릴 수 있는지 확인해줘 +``` + +수행 방식: + +- controller/service/repository/query 흐름을 따라간다. +- pagination, index, N+1, transaction 범위, 외부 호출을 확인한다. +- 개선안은 운영 영향과 적용 난이도 순서로 정리한다. + +### explain + +코드 흐름이나 설계 판단을 학습 목적으로 설명받을 때 사용한다. + +```text +$explain StudyGroup 가입 요청 API 흐름 설명해줘 +``` + +수행 방식: + +- 진입점부터 service, repository, 응답 DTO까지 호출 경로를 추적한다. +- 중요한 근거는 파일과 라인으로 설명한다. +- API 계약, validation, 예외 처리, 트랜잭션, 보안 포인트를 함께 짚는다. + +## 4. 빌트인 리뷰 사용 + +Codex CLI에는 리뷰 명령이 있다. + +대화형 Codex 안에서: + +```text +/review +``` + +터미널에서 비대화형으로: + +```bash +codex --profile-v2 review review +``` + +OnO 백엔드 변경 검토 시에는 이런 기준을 함께 요청한다. + +```text +사용자 데이터 격리, 권한 누락, N+1, 풀스캔, 트랜잭션, FCM/메일 실발송, DB schema 안전성 기준으로 위험도를 나눠 검토해줘. 발견한 문제는 파일과 라인 근거를 포함해줘. +``` + +## 5. codex exec 사용 + +반복 검증이나 릴리즈 게이트처럼 비대화형 실행이 필요할 때 사용한다. + +```bash +codex --profile-v2 review exec "현재 변경을 OnO 위험 체크리스트로 검토하고, 사용자 데이터 손상이나 권한 누락 가능성이 있으면 명확히 보고해줘" +``` + +백엔드 배포 전 예: + +```bash +codex --profile-v2 review exec "현재 변경을 데이터 격리, N+1, 풀스캔, 트랜잭션, FCM/메일 실발송, ddl-auto 변경 기준으로 검토해줘" +``` + +구현 작업을 비대화형으로 맡길 때는 `build`를 사용한다. + +```bash +codex --profile-v2 build exec "docs/<기능폴더>/<명세서>.md 기준으로 구현하고 관련 테스트를 실행해줘" +``` + +운영 서비스 변경은 비대화형 구현보다 대화형 `build` 모드가 더 안전하다. + +## 6. 작업 완료 알림 + +전역 config의 `notify`는 다음 래퍼를 사용한다. + +```text +~/.codex/scripts/codex-notify-macos.sh +``` + +동작: + +- 기존 Computer Use `turn-ended` 알림을 그대로 호출한다. +- 추가로 macOS 알림을 띄운다. +- macOS 알림 실패 시 터미널 벨로 폴백한다. + +수동 테스트: + +```bash +~/.codex/scripts/codex-notify-macos.sh \ + "/Users/ksm/.codex/computer-use/Codex Computer Use.app/Contents/SharedSupport/SkyComputerUseClient.app/Contents/MacOS/SkyComputerUseClient" \ + turn-ended \ + '{"type":"agent-turn-complete"}' +``` + +알림이 보이지 않으면 macOS 시스템 설정에서 Terminal, Codex, 또는 사용 중인 터미널 앱의 알림 권한을 확인한다. + +## 7. 설정 동기화 + +이 레포의 `.codex`를 수정한 뒤 실제 Codex 설정에도 반영하려면 같은 내용을 `~/.codex`로 복사해야 한다. + +```bash +cp .codex/review.config.toml ~/.codex/review.config.toml +cp .codex/build.config.toml ~/.codex/build.config.toml +cp -R .codex/skills/analysis ~/.codex/skills/ +cp -R .codex/skills/check ~/.codex/skills/ +cp -R .codex/skills/commit ~/.codex/skills/ +cp -R .codex/skills/feat ~/.codex/skills/ +cp -R .codex/skills/plan ~/.codex/skills/ +cp -R .codex/skills/report ~/.codex/skills/ +cp -R .codex/skills/test ~/.codex/skills/ +cp -R .codex/skills/test-writer ~/.codex/skills/ +cp -R .codex/skills/perf ~/.codex/skills/ +cp -R .codex/skills/perf-audit ~/.codex/skills/ +cp -R .codex/skills/explain ~/.codex/skills/ +cp .codex/scripts/codex-notify-macos.sh ~/.codex/scripts/codex-notify-macos.sh +chmod +x ~/.codex/scripts/codex-notify-macos.sh +``` + +현재 전역 config 백업 파일: + +```text +~/.codex/config.toml.bak.codex-setup-kit-20260604 +``` + +## 8. 추천 작업 흐름 + +새 기능: + +```text +codex --profile-v2 review +plan <기능 설명> +``` + +이후: + +```text +codex --profile-v2 build +feat <작성된 명세서 경로> +``` + +마지막 검토: + +```text +codex --profile-v2 review +/review +``` + +커밋 메시지 형식: + +```text +[Feat] 구현 타이틀 - 구현 상세1 구현 상세2 +``` + +버그 수정: + +```text +codex --profile-v2 build +$test-writer 버그 재현 테스트를 먼저 추가하고 수정해줘 +``` + +성능 점검: + +```text +codex --profile-v2 review +$perf-audit 느린 API 후보를 쿼리와 트랜잭션 기준으로 점검해줘 +``` diff --git a/.codex/build.config.toml b/.codex/build.config.toml new file mode 100644 index 00000000..5491007e --- /dev/null +++ b/.codex/build.config.toml @@ -0,0 +1,3 @@ +model_reasoning_effort = "high" +sandbox_mode = "workspace-write" +approval_policy = "on-request" diff --git a/.codex/review.config.toml b/.codex/review.config.toml new file mode 100644 index 00000000..7820deaf --- /dev/null +++ b/.codex/review.config.toml @@ -0,0 +1,3 @@ +model_reasoning_effort = "xhigh" +sandbox_mode = "read-only" +approval_policy = "on-request" diff --git a/.codex/scripts/codex-notify-macos.sh b/.codex/scripts/codex-notify-macos.sh new file mode 100755 index 00000000..dbf6104d --- /dev/null +++ b/.codex/scripts/codex-notify-macos.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -u + +sky_client="${1:-}" +sky_event="${2:-turn-ended}" +payload="${3:-}" + +if [ -n "$sky_client" ] && [ -x "$sky_client" ]; then + "$sky_client" "$sky_event" "$payload" >/dev/null 2>&1 & +fi + +message="Codex 작업이 완료되었습니다." +title="Codex" + +if ! /usr/bin/osascript -e "display notification \"$message\" with title \"$title\" sound name \"Glass\"" >/dev/null 2>&1; then + printf '\a' +fi diff --git a/.codex/skills/analysis/SKILL.md b/.codex/skills/analysis/SKILL.md new file mode 100644 index 00000000..928b487e --- /dev/null +++ b/.codex/skills/analysis/SKILL.md @@ -0,0 +1,28 @@ +--- +name: analysis +description: OnO 문제 원인 규명과 코드 분석이 필요할 때 사용. 사용자가 "analysis <증상>" 또는 원인 분석을 요청하면 코드를 수정하지 않고 보고한다. 인자가 없으면 현재 git 변경사항을 대상으로 분석한다. +--- + +# OnO Analysis + +## Workflow + +1. 인자가 없으면 `git status`와 `git diff HEAD` 기준으로 현재 변경사항을 분석 대상으로 삼는다. +2. 증상, 원하는 결과, 관련 파일/로그가 불명확하고 현재 변경사항에서도 분석 대상이 좁혀지지 않으면 먼저 짧게 확인한다. +3. 가능한 원인 가설을 3~5개로 나눈다. +4. 관련 코드 흐름, 설정, 로그, git diff/log를 읽기 전용으로 조사한다. +5. 각 가설을 `파일:라인` 근거로 지지하거나 반박한다. +6. 1차 결론을 스스로 반증한다. 상위 레이어에서 막히는지, 실제 호출 가능한지, 다른 설정이 우선하는지 확인한다. +7. 완료 직전에 아래 자가 반증 질문에 답하고, 필요한 경우 결론을 수정한다. + - 내가 놓쳤을 가능성이 큰 것은? + - 이 변경이 실제 사용자에게 깨질 수 있는 경로는? + - 이 결론이 틀렸다면 어떤 조건 때문인가? +8. 코드는 수정하지 않는다. + +## Output + +- 한 문장 요약 +- 근거 목록 +- 근본 원인과 확신도 +- 자가 반증 결과 +- 다음 단계 제안 diff --git a/.codex/skills/analysis/agents/openai.yaml b/.codex/skills/analysis/agents/openai.yaml new file mode 100644 index 00000000..d82b49df --- /dev/null +++ b/.codex/skills/analysis/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "OnO Analysis" + short_description: "OnO 문제 원인 분석" + default_prompt: "Use $analysis to investigate this OnO issue." + +policy: + allow_implicit_invocation: true diff --git a/.codex/skills/check/SKILL.md b/.codex/skills/check/SKILL.md new file mode 100644 index 00000000..ae9f4ae0 --- /dev/null +++ b/.codex/skills/check/SKILL.md @@ -0,0 +1,44 @@ +--- +name: check +description: OnO 변경사항을 읽기 전용으로 다관점 검증할 때 사용. 사용자가 "check", "check <커밋>", 위험 검토, 배포 전 검토를 요청하면 diff와 영향 흐름을 2중으로 검토한다. 인자가 없으면 현재 git 변경사항을 대상으로 한다. +--- + +# OnO Check + +## Scope + +- 인자가 있으면 해당 커밋/범위부터 현재까지 확인한다. +- 인자가 없으면 `git status`와 `git diff HEAD` 기준의 현재 변경사항을 확인한다. +- 코드는 수정하지 않는다. + +## Workflow + +1. `git status`, `git diff`, 필요 시 `git log/show`로 변경 범위를 좁힌다. +2. 변경된 파일과 영향받는 사용자 흐름 기준으로 검토한다. +3. 1차 검토: + - 사용자 데이터 격리, 인증/인가 누락 + - N+1, 풀스캔, pagination/index, 커넥션 풀 위험 + - null/empty, 예외 처리, 롤백, 동시성, 비즈니스 정합성 + - FCM/메일 실발송, 잘못된 DB write, schema 안전성 +4. 2차 반증: + - 발견한 문제가 실제로 도달 가능한지 확인한다. + - 상위 validation, 권한 체크, transaction 경계가 이미 막는지 확인한다. + - 반대로 1차에서 놓친 사용자 영향 흐름이 있는지 확인한다. +5. 완료 직전에 아래 자가 반증 질문에 답하고, 필요한 경우 위험도 판단을 수정한다. + - 내가 놓쳤을 가능성이 큰 것은? + - 이 변경이 실제 사용자에게 깨질 수 있는 경로는? + - 이 결론이 틀렸다면 어떤 조건 때문인가? +6. 가능한 경우 관련 테스트/정적 검사/빌드 명령을 제안하거나 실행한다. 읽기 전용 프로파일이면 실행 가능 범위만 명시한다. + +## Output + +위험도 순서로 보고한다. + +- 🔴 즉시 수정 필요 +- 🟠 배포 전 수정 권장 +- 🟡 다음 이터레이션 +- 🟢 이상 없음 + +각 항목은 `파일:라인`, 사용자 영향, 수정 방향을 포함한다. + +마지막에 자가 반증 결과를 짧게 포함한다. diff --git a/.codex/skills/check/agents/openai.yaml b/.codex/skills/check/agents/openai.yaml new file mode 100644 index 00000000..10c1dcfd --- /dev/null +++ b/.codex/skills/check/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "OnO Check" + short_description: "OnO 변경 위험 검토" + default_prompt: "Use $check to review this OnO change." + +policy: + allow_implicit_invocation: true diff --git a/.codex/skills/commit/SKILL.md b/.codex/skills/commit/SKILL.md new file mode 100644 index 00000000..178da7ad --- /dev/null +++ b/.codex/skills/commit/SKILL.md @@ -0,0 +1,33 @@ +--- +name: commit +description: 현재 변경사항의 커밋 단위와 OnO 커밋 메시지를 제안할 때 사용. 인자가 없어도 현재 git 변경사항을 대상으로 한다. 실제 git commit은 사용자가 명시적으로 요청하기 전까지 실행하지 않는다. +--- + +# OnO Commit + +## Workflow + +1. `git status`와 `git diff HEAD`로 현재 변경 전체를 파악한다. +2. 사용자 변경과 이번 작업 변경을 구분한다. 임의로 되돌리지 않는다. +3. 변경을 독립적으로 빌드/검토 가능한 논리 단위로 나눈다. +4. 각 단위에 커밋 메시지를 제안한다. +5. 실제 `git add` 또는 `git commit`은 사용자가 명시적으로 요청한 경우에만 수행한다. + +## Message Format + +```text +[Feat] 구현 타이틀 - 구현 상세1 구현 상세2 +``` + +태그 후보: + +```text +[Feat] [Fix] [Refactor] [Chore] [Test] [Docs] [Perf] +``` + +## Output + +- 추천 커밋 단위 +- 각 단위에 포함할 파일 +- 커밋 메시지 +- 필요한 경우 커밋 순서 diff --git a/.codex/skills/commit/agents/openai.yaml b/.codex/skills/commit/agents/openai.yaml new file mode 100644 index 00000000..6f971354 --- /dev/null +++ b/.codex/skills/commit/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "OnO Commit" + short_description: "OnO 커밋 단위 제안" + default_prompt: "Use $commit to suggest commit units." + +policy: + allow_implicit_invocation: true diff --git a/.codex/skills/explain/SKILL.md b/.codex/skills/explain/SKILL.md new file mode 100644 index 00000000..0fb684b8 --- /dev/null +++ b/.codex/skills/explain/SKILL.md @@ -0,0 +1,20 @@ +--- +name: explain +description: 사용자가 OnO 백엔드 또는 Flutter 코드 흐름, 원리, 호출 경로, 설계 판단을 학습 목적으로 설명해 달라고 요청할 때 사용한다. +--- + +# OnO Explain + +## Workflow + +1. 추측으로 설명하지 않고 관련 파일과 호출 경로를 먼저 확인한다. +2. 설명은 `요청 진입점 -> 핵심 로직 -> DB/API/외부 연동 -> 응답/상태 반영` 순서로 정리한다. +3. 중요한 근거는 파일 경로와 라인으로 연결한다. +4. 백엔드 개발자 관점에서 API 계약, DTO, validation, 예외 처리, 트랜잭션, 보안, 성능 포인트를 짧게 짚는다. +5. 개선 제안은 현재 코드의 실제 리스크가 있을 때만, tradeoff와 함께 제시한다. + +## Output + +- 먼저 전체 흐름을 짧게 요약한다. +- 그다음 핵심 파일별 역할을 설명한다. +- 마지막에 주의할 운영 리스크나 학습 포인트를 정리한다. diff --git a/.codex/skills/explain/agents/openai.yaml b/.codex/skills/explain/agents/openai.yaml new file mode 100644 index 00000000..5b93dce5 --- /dev/null +++ b/.codex/skills/explain/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "OnO Explain" + short_description: "OnO 코드 흐름 학습 설명" + default_prompt: "Use $explain to explain this OnO code path." + +policy: + allow_implicit_invocation: true diff --git a/.codex/skills/feat/SKILL.md b/.codex/skills/feat/SKILL.md new file mode 100644 index 00000000..6aa943cf --- /dev/null +++ b/.codex/skills/feat/SKILL.md @@ -0,0 +1,31 @@ +--- +name: feat +description: Use for OnO feature implementation. Trigger when the user requests "feat" or plan-based implementation. If no argument is provided, inspect current git changes and continue with remaining implementation, cleanup, and validation work. +--- + +# OnO Feat + +## Workflow + +1. 기준 구현 명세서나 작업 설명을 확인한다. +2. 인자가 없으면 `git status`와 `git diff HEAD` 기준으로 현재 변경사항을 먼저 읽고, 이어서 구현해야 할 남은 작업이나 검증 대상을 파악한다. +3. 기준 문서가 불명확하고 현재 변경사항만으로 구현 의도가 확정되지 않으면 어떤 명세서를 사용할지 사용자에게 묻는다. +4. 명세서와 실제 코드가 다르면 실제 코드 기준으로 판단하고 차이를 설명한다. +5. 구현 범위를 명세서 또는 현재 변경사항의 의도에 맞춰 제한한다. 임의 확장, 과한 추상화, 무관한 리팩터링을 하지 않는다. +6. 백엔드 구현 시 API 계약, DTO, validation, 예외 처리, transaction, 권한, 성능, 로그, 데이터 정합성을 확인한다. +7. DB schema 변경은 migration/명시 SQL을 우선한다. H2 인메모리가 아닌 환경에 `ddl-auto=create` 또는 `create-drop`을 추가하지 않는다. +8. 기능 위험도에 맞춰 단위 테스트 또는 API 통합 테스트를 추가/갱신한다. +9. 가능한 범위에서 포맷팅, 정적 검사, 관련 테스트, 빌드를 실행한다. +10. 완료 직전에 아래 자가 반증 질문에 답하고, 필요한 경우 추가 수정 또는 리스크 보고를 한다. + - 내가 놓쳤을 가능성이 큰 것은? + - 이 변경이 실제 사용자에게 깨질 수 있는 경로는? + - 이 결론이 틀렸다면 어떤 조건 때문인가? + +## Completion Report + +- 변경한 내용 +- 사용자에게 미치는 영향 +- 실행한 검증과 결과 +- 자가 반증 결과 +- 남은 리스크 또는 확인 질문 +- 추천 커밋 단위와 커밋 메시지 diff --git a/.codex/skills/feat/agents/openai.yaml b/.codex/skills/feat/agents/openai.yaml new file mode 100644 index 00000000..12a1092c --- /dev/null +++ b/.codex/skills/feat/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "OnO Feat" + short_description: "OnO 기능 구현 워크플로우" + default_prompt: "Use $feat to implement this OnO feature." + +policy: + allow_implicit_invocation: true diff --git a/.codex/skills/perf-audit/SKILL.md b/.codex/skills/perf-audit/SKILL.md new file mode 100644 index 00000000..fdfe2139 --- /dev/null +++ b/.codex/skills/perf-audit/SKILL.md @@ -0,0 +1,28 @@ +--- +name: perf-audit +description: OnO 백엔드 API 성능, 느린 쿼리, N+1, 풀스캔, 커넥션 풀, Flutter rebuild/jank/메모리 문제가 의심될 때 사용한다. +--- + +# OnO Performance Audit + +## Workflow + +1. 사용자가 체감하는 느린 흐름과 호출 경로를 먼저 좁힌다. +2. 백엔드는 controller/service/repository/query/transaction 경계를 따라간다. +3. Flutter는 화면 진입, 상태 변경, async 경합, rebuild 범위를 따라간다. +4. 운영 영향 기준으로 우선순위를 나눈다. + - 사용자 데이터 손상 또는 장애 가능성 + - API latency, DB 부하, 커넥션 고갈 가능성 + - UI jank, 중복 요청, 배터리/메모리 낭비 +5. 개선안은 관측 가능한 근거와 함께 제시하고, 과한 구조 변경은 피한다. + +## Backend Focus + +- N+1, unbounded query, pagination 누락, index 부재, 풀스캔 가능성 +- 트랜잭션 범위 과다/부족, lazy loading 도달 가능성 +- 외부 호출, FCM/메일 실발송, retry 부재 + +## Flutter Focus + +- 불필요한 rebuild, 큰 리스트의 비효율 렌더링, debounce/throttle 부재 +- loading/error/empty 상태에서 중복 요청 또는 화면 멈춤 diff --git a/.codex/skills/perf-audit/agents/openai.yaml b/.codex/skills/perf-audit/agents/openai.yaml new file mode 100644 index 00000000..f684fdfa --- /dev/null +++ b/.codex/skills/perf-audit/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "OnO Perf Audit" + short_description: "OnO 성능 리스크 점검" + default_prompt: "Use $perf-audit to inspect this OnO performance concern." + +policy: + allow_implicit_invocation: true diff --git a/.codex/skills/perf/SKILL.md b/.codex/skills/perf/SKILL.md new file mode 100644 index 00000000..981b4538 --- /dev/null +++ b/.codex/skills/perf/SKILL.md @@ -0,0 +1,22 @@ +--- +name: perf +description: OnO 성능 병목 진단 요청에 사용. 사용자가 "perf" 명령을 요청하면 perf-audit 원칙에 따라 읽기 전용으로 호출 경로와 쿼리/렌더링 병목을 분석한다. 인자가 없으면 현재 git 변경사항이 건드린 흐름을 대상으로 한다. +--- + +# OnO Perf + +## Workflow + +1. 대상 기능, 엔드포인트, 패키지, 화면을 확인한다. 인자가 없으면 `git status`와 `git diff HEAD` 기준으로 현재 변경사항이 건드린 흐름을 대상으로 삼는다. +2. 백엔드는 controller/service/repository/query 흐름과 transaction 경계를 추적한다. +3. Flutter는 화면 진입, 상태 변경, async 경합, rebuild 범위를 추적한다. +4. N+1, fetch 전략, 풀스캔, pagination 누락, index 부재, 커넥션 풀 고갈, 데드락 가능성을 확인한다. +5. 읽기 전용으로 진단하고 코드는 수정하지 않는다. + +## Output + +- 🔴 즉시 개선 +- 🟠 배포 전 개선 권장 +- 🟡 백로그 + +각 개선안은 `파일:라인`, 원인, 개선 방법, 예상 효과를 포함한다. diff --git a/.codex/skills/perf/agents/openai.yaml b/.codex/skills/perf/agents/openai.yaml new file mode 100644 index 00000000..058eb015 --- /dev/null +++ b/.codex/skills/perf/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "OnO Perf" + short_description: "OnO 성능 병목 진단" + default_prompt: "Use $perf to audit this performance issue." + +policy: + allow_implicit_invocation: true diff --git a/.codex/skills/plan/SKILL.md b/.codex/skills/plan/SKILL.md new file mode 100644 index 00000000..499e8e33 --- /dev/null +++ b/.codex/skills/plan/SKILL.md @@ -0,0 +1,29 @@ +--- +name: plan +description: OnO 백엔드 또는 Flutter 기능을 구현하기 전에 구현 명세서가 필요할 때 사용. 사용자가 "plan <기능 설명>"을 요청하면 코드를 수정하지 않고 계획서를 작성한다. 인자가 없으면 현재 git 변경사항을 대상으로 후속 구현/정리 계획서를 작성한다. +--- + +# OnO Plan + +## Workflow + +1. 인자가 없으면 `git status`와 `git diff HEAD` 기준으로 현재 변경사항의 목적과 영향 범위를 먼저 파악한다. +2. 요청 기능 또는 현재 변경사항의 사용자 시나리오와 운영 영향 범위를 정리한다. +3. 관련 코드, API 계약, DB schema, 인증/권한, 상태 흐름, 기존 문서를 확인한다. +4. 코드는 구현하지 않는다. 문서만 작성한다. +5. `/Users/ksm/programing/sw_maestro/OnO_BACKEND/backend/docs` 아래에 기능을 알아볼 수 있는 새 폴더를 만든다. +6. 구현 명세서에는 다음 항목을 포함한다. + - 기능 목적과 사용자 시나리오 + - 구현 범위와 제외 범위 + - 관련 화면, 라우팅, 상태 관리, API 영향 + - 데이터 흐름과 에러 처리 + - 구현 단계 + - 검증 방법 + - 배포 리스크와 확인 질문 +7. 이후 `feat` 요청에서 바로 구현할 수 있을 정도로 파일, 클래스, 테스트 방향을 구체화한다. + +## OnO 기준 + +- 실사용자 데이터, 학습 기록, 인증, 결제, 동기화, 알림, API 호환성을 최우선으로 본다. +- DB schema 변경이 필요하면 `ddl-auto=create` 또는 `create-drop`을 제안하지 않는다. +- 불확실한 API 계약이나 null 가능성은 추측하지 말고 확인 질문으로 남긴다. diff --git a/.codex/skills/plan/agents/openai.yaml b/.codex/skills/plan/agents/openai.yaml new file mode 100644 index 00000000..61dbc07f --- /dev/null +++ b/.codex/skills/plan/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "OnO Plan" + short_description: "OnO 기능 구현 명세서 작성" + default_prompt: "Use $plan to draft an OnO implementation plan." + +policy: + allow_implicit_invocation: true diff --git a/.codex/skills/report/SKILL.md b/.codex/skills/report/SKILL.md new file mode 100644 index 00000000..a4a7a7c5 --- /dev/null +++ b/.codex/skills/report/SKILL.md @@ -0,0 +1,24 @@ +--- +name: report +description: 특정 커밋부터 현재까지의 OnO 작업 결과 보고서 또는 PR 초안을 작성할 때 사용. 인자가 없으면 현재 git 변경사항을 대상으로 변경 분석, 사용자 영향, 검증 결과, 배포 리스크를 markdown으로 정리한다. +--- + +# OnO Report + +## Workflow + +1. 기준 커밋 해시 또는 변경 범위를 확인한다. 인자가 없으면 현재 git 변경사항을 대상으로 한다. +2. `git log`, `git diff`, `git status`로 커밋된 변경과 미커밋 변경을 구분한다. +3. 수정된 파일, 레이어, API/DB/인증/알림 영향 범위를 정리한다. +4. Before/After가 의미 있는 변경은 표로 요약한다. +5. 테스트 코드는 세부 구현보다 검증 결과 중심으로 적는다. +6. 저장 위치가 지정되지 않았으면 사용자에게 확인한다. OnO 백엔드 문서는 기본적으로 `/Users/ksm/programing/sw_maestro/OnO_BACKEND/backend/docs` 아래에 둔다. + +## Output + +- 변경사항 요약 +- 사용자 영향 +- 검증 결과 +- 배포 리스크와 롤백/대응 방법 +- 미커밋 변경 반영 여부 +- 필요 시 PR 제목/본문 초안 diff --git a/.codex/skills/report/agents/openai.yaml b/.codex/skills/report/agents/openai.yaml new file mode 100644 index 00000000..b8845bd2 --- /dev/null +++ b/.codex/skills/report/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "OnO Report" + short_description: "OnO 변경 보고서 작성" + default_prompt: "Use $report to summarize this OnO change." + +policy: + allow_implicit_invocation: true diff --git a/.codex/skills/test-writer/SKILL.md b/.codex/skills/test-writer/SKILL.md new file mode 100644 index 00000000..029751bc --- /dev/null +++ b/.codex/skills/test-writer/SKILL.md @@ -0,0 +1,23 @@ +--- +name: test-writer +description: OnO 백엔드 또는 Flutter 테스트를 새로 작성하거나 보강할 때 사용. Spring/JUnit/API 통합 테스트, Flutter widget/unit 테스트, 버그 재현 테스트가 필요한 요청에서 자동으로 사용한다. +--- + +# OnO Test Writer + +## Workflow + +1. 변경된 동작을 검증 가능한 성공/실패 조건으로 바꾼다. +2. 기존 테스트 구조, test profile, fixture, 인증 helper, DB 격리 방식을 먼저 확인한다. +3. 버그 수정이면 가능하면 실패하는 테스트를 먼저 추가하고 수정 후 통과시킨다. +4. 테스트 범위는 위험도에 맞춘다. + - 단위 테스트: validation, 예외 분기, 데이터 변환, 핵심 비즈니스 로직 + - API 통합 테스트: 요청/응답, 인증/권한, validation 실패, DB 반영, 에러 응답 + - Flutter 테스트: null/empty/error/loading 상태와 핵심 위젯 렌더링 +5. 테스트가 어려운 구조이면 이유와 대체 검증을 명확히 남긴다. + +## OnO 체크 + +- 사용자별 데이터 격리와 권한 실패 케이스를 우선 확인한다. +- 검색, 오답노트, 스터디그룹, 결제, 알림, 동기화 흐름은 성공 케이스만으로 끝내지 않는다. +- MySQL/PostgreSQL 기반 환경에서 테스트 편의를 위해 `ddl-auto=create` 또는 `create-drop`을 추가하지 않는다. diff --git a/.codex/skills/test-writer/agents/openai.yaml b/.codex/skills/test-writer/agents/openai.yaml new file mode 100644 index 00000000..704546fb --- /dev/null +++ b/.codex/skills/test-writer/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "OnO Test Writer" + short_description: "OnO 테스트 작성과 보강" + default_prompt: "Use $test-writer to add focused tests for this OnO change." + +policy: + allow_implicit_invocation: true diff --git a/.codex/skills/test/SKILL.md b/.codex/skills/test/SKILL.md new file mode 100644 index 00000000..cffc379e --- /dev/null +++ b/.codex/skills/test/SKILL.md @@ -0,0 +1,21 @@ +--- +name: test +description: OnO 테스트 작성 및 실행 요청에 사용. 사용자가 "test" 명령을 요청하면 test-writer 원칙에 따라 기존 테스트 구조를 확인하고 필요한 테스트를 작성/실행한다. 인자가 없으면 현재 git 변경사항을 대상으로 한다. +--- + +# OnO Test + +## Workflow + +1. 대상이 없으면 `git status`와 `git diff HEAD` 기준의 현재 변경사항으로 테스트 대상을 좁힌다. +2. 기존 테스트 구조, test profile, fixture, 인증 helper, DB 격리 방식을 확인한다. +3. 필요한 테스트를 작성하거나 보강한다. +4. 사용자 격리, 권한 실패, validation 실패, null/empty, 예외 케이스를 위험도에 맞춰 포함한다. +5. 외부 연동(FCM, 메일 등)은 mock 또는 fake로 검증한다. +6. 관련 테스트만 우선 실행하고, 실패하면 원인 파악 후 수정한다. + +## Guardrails + +- MySQL/PostgreSQL 환경에 `ddl-auto=create` 또는 `create-drop`을 추가하지 않는다. +- 기존 저품질 테스트를 그대로 복붙하지 않는다. +- 테스트 실행이 어렵다면 이유와 대체 검증을 보고한다. diff --git a/.codex/skills/test/agents/openai.yaml b/.codex/skills/test/agents/openai.yaml new file mode 100644 index 00000000..96e72414 --- /dev/null +++ b/.codex/skills/test/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "OnO Test" + short_description: "OnO 테스트 작성 실행" + default_prompt: "Use $test to write and run focused tests." + +policy: + allow_implicit_invocation: true From 363df80096fddb13773a6642324e43d45d072c53 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 4 Jun 2026 23:08:15 +0900 Subject: [PATCH 04/67] =?UTF-8?q?[Feat]=20DB=20=EC=95=88=EC=A0=84=EC=84=B1?= =?UTF-8?q?=20=ED=9B=85=20=EC=B0=A8=EB=8B=A8=20=ED=8C=A8=ED=84=B4=20?= =?UTF-8?q?=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 스키마 초기화 명령(flyway의 clean 서브커맨드) 감지 추가 - JPA ddl-auto 위험 모드(create/create-drop) 감지 추가 - 볼륨 삭제 플래그(-v) 포함한 compose down 차단 - 프로덕션 DB 직접 접속 명령 차단 - WHERE 절 없는 UPDATE 감지 추가 --- .claude/hooks/db_safety_check.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.claude/hooks/db_safety_check.py b/.claude/hooks/db_safety_check.py index 5eb6976e..87b38764 100644 --- a/.claude/hooks/db_safety_check.py +++ b/.claude/hooks/db_safety_check.py @@ -18,6 +18,12 @@ (r"TRUNCATE\s+(TABLE\s+)?[`\w][`\w.]*", "TRUNCATE"), (r"ALTER\s+TABLE\s+[`\w][`\w.]*\s+DROP", "DROP COLUMN/INDEX"), (r"DROP\s+DATABASE", "DROP DATABASE"), + (r"flyway\s+clean\b", "Flyway clean"), + (r"ddl-auto\s*[:=]\s*create-drop\b", "ddl-auto=create-drop"), + (r"ddl-auto\s*[:=]\s*create\b", "ddl-auto=create"), + (r"docker\s+compose\s+down\b.*\s-v\b", "docker compose down -v"), + (r"docker-compose\b.*\sdown\b.*\s-v\b", "docker-compose down -v"), + (r"\bmysql\b.*\b(prod|production)\b", "production mysql command"), ] found = [label for pattern, label in DANGEROUS if re.search(pattern, cmd, re.IGNORECASE)] @@ -27,8 +33,13 @@ and not re.search(r"\bWHERE\b", cmd, re.IGNORECASE)): found.append("WHERE 없는 DELETE") +# WHERE 없는 UPDATE는 운영 데이터 대량 변경으로 이어질 수 있다. +if (re.search(r"\bUPDATE\s+[`.\w]+\s+SET\b", cmd, re.IGNORECASE) + and not re.search(r"\bWHERE\b", cmd, re.IGNORECASE)): + found.append("WHERE 없는 UPDATE") + if found: print(f"🚨 위험한 DB 작업 감지: {', '.join(found)}") print(f" 명령: {cmd[:200]}") print(" 프로덕션 영향 범위를 확인한 뒤 명시적으로 허용해 주세요.") - sys.exit(2) \ No newline at end of file + sys.exit(2) From ca74a16b7e687ed3a07547ae2f91b3b24c436deb Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 4 Jun 2026 23:08:21 +0900 Subject: [PATCH 05/67] =?UTF-8?q?[Docs]=20PR=20=ED=85=9C=ED=94=8C=EB=A6=BF?= =?UTF-8?q?=20=EC=84=B9=EC=85=98=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자 영향, API 호환성, DB migration 체크리스트 추가 - 인증/권한, 검증 결과, 배포 리스크, 롤백 방법 섹션 추가 --- .github/PULL_REQUEST_TEMPLATE.md | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0bc873a4..50e06d7b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,4 +6,37 @@ > 이번 PR에서 작업한 내용을 간략히 설명해주세요 +## 👤 사용자 영향 + +- + +## 🔌 API 호환성 + +- [ ] 요청/응답 DTO 변경 없음 +- [ ] 기존 Flutter 앱 버전과 호환 확인 +- [ ] breaking change 있음 + +## 🗄️ DB migration + +- [ ] 없음 +- [ ] 있음: migration 파일과 기존 데이터 호환성 확인 + +## 🔐 인증/권한 + +- [ ] 영향 없음 +- [ ] 사용자별 데이터 소유권 검증 확인 +- [ ] 관리자/특수 권한 영향 확인 + +## ✅ 검증 결과 + +- + +## 🚀 배포 리스크 + +- + +## ↩️ 롤백/대응 방법 + +- + ### 스크린샷 (선택) From c4e306fc780b0d081339c639da5ab0087a651ea6 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 4 Jun 2026 23:08:36 +0900 Subject: [PATCH 06/67] =?UTF-8?q?[Chore]=20Claude=20Code=20/=20Codex=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLAUDE.md: 자유 작업 범위를 읽기·로컬 검증으로 한정, git 작업은 명시 요청 시에만 수행 - CLAUDE.md: 인프라·외부 발송·되돌리기 어려운 작업 사전 확인 절차 명확화 - .gitignore: /AGENTS.md 항목 제거 - .codex/USAGE.md: migrate, pr 스킬 추가 및 문서화 - .codex/skills/check/SKILL.md: description 영문화, 불필요 공백 제거 --- .codex/USAGE.md | 35 ++++++++++++++++++++++++++++++++++- .codex/skills/check/SKILL.md | 7 +++---- .gitignore | 1 - CLAUDE.md | 9 +++++---- 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/.codex/USAGE.md b/.codex/USAGE.md index 1d031a30..1096871d 100644 --- a/.codex/USAGE.md +++ b/.codex/USAGE.md @@ -14,7 +14,9 @@ ├── check/ ├── commit/ ├── feat/ + ├── migrate/ ├── plan/ + ├── pr/ ├── report/ ├── test/ ├── test-writer/ @@ -32,7 +34,9 @@ ~/.codex/skills/check ~/.codex/skills/commit ~/.codex/skills/feat +~/.codex/skills/migrate ~/.codex/skills/plan +~/.codex/skills/pr ~/.codex/skills/report ~/.codex/skills/test ~/.codex/skills/test-writer @@ -115,7 +119,7 @@ feat docs/...의 구현 명세서를 기준으로 기능 구현해줘 현재 `.claude/commands`와 맞춰 제공하는 주요 명령어 이름: ```text -analysis check commit explain feat perf plan report test +analysis check commit explain feat migrate perf plan pr report test ``` 보조 스킬: @@ -246,6 +250,35 @@ plan 학습 기록 검색 API 필터 기능 - API 계약, 데이터 흐름, 에러 처리, 테스트 방법, 배포 리스크를 정리한다. - `/Users/ksm/programing/sw_maestro/OnO_BACKEND/backend/docs` 아래 기능별 폴더에 markdown을 작성한다. +### migrate + +DB schema 변경과 Flyway migration이 필요할 때 사용한다. + +```text +$migrate review_schedule에 알림 실패 사유 컬럼 추가 +``` + +수행 방식: + +- 기존 migration 버전과 관련 entity/repository/query/API 영향을 먼저 확인한다. +- H2 인메모리가 아닌 환경에 `ddl-auto=create` 또는 `create-drop`을 추가하지 않는다. +- 안전한 migration 순서, backfill, rollback/대응 방법, dev/prod 적용 리스크를 정리한다. +- 운영 영향이 있는 SQL 파일은 저장 전 확인한다. + +### pr + +기준 커밋부터 현재까지의 변경사항으로 PR 제목과 본문 초안을 작성할 때 사용한다. + +```text +$pr abc1234 +``` + +수행 방식: + +- `git log`, `git diff`, `git status`로 커밋된 변경과 미커밋 변경을 분리한다. +- `.github/PULL_REQUEST_TEMPLATE.md`를 읽고 같은 구조로 작성한다. +- 사용자 영향, API 호환성, DB migration, 인증/권한, 검증 결과, 배포 리스크, 롤백 방법을 포함한다. + ### test-writer 테스트 구현 원칙을 더 강하게 적용할 때 사용하는 보조 스킬이다. diff --git a/.codex/skills/check/SKILL.md b/.codex/skills/check/SKILL.md index ae9f4ae0..697b534c 100644 --- a/.codex/skills/check/SKILL.md +++ b/.codex/skills/check/SKILL.md @@ -1,6 +1,6 @@ --- name: check -description: OnO 변경사항을 읽기 전용으로 다관점 검증할 때 사용. 사용자가 "check", "check <커밋>", 위험 검토, 배포 전 검토를 요청하면 diff와 영향 흐름을 2중으로 검토한다. 인자가 없으면 현재 git 변경사항을 대상으로 한다. +description: Use for read-only OnO change review. Trigger on "check", "check ", risk review, or pre-deploy review. If no argument is provided, review current git changes. --- # OnO Check @@ -17,8 +17,8 @@ description: OnO 변경사항을 읽기 전용으로 다관점 검증할 때 사 2. 변경된 파일과 영향받는 사용자 흐름 기준으로 검토한다. 3. 1차 검토: - 사용자 데이터 격리, 인증/인가 누락 - - N+1, 풀스캔, pagination/index, 커넥션 풀 위험 - - null/empty, 예외 처리, 롤백, 동시성, 비즈니스 정합성 + - N+1, 풀스캔, pagination/index, connection pool 위험 + - null/empty, 예외 처리, rollback, 동시성, 비즈니스 정합성 - FCM/메일 실발송, 잘못된 DB write, schema 안전성 4. 2차 반증: - 발견한 문제가 실제로 도달 가능한지 확인한다. @@ -40,5 +40,4 @@ description: OnO 변경사항을 읽기 전용으로 다관점 검증할 때 사 - 🟢 이상 없음 각 항목은 `파일:라인`, 사용자 영향, 수정 방향을 포함한다. - 마지막에 자가 반증 결과를 짧게 포함한다. diff --git a/.gitignore b/.gitignore index 30cb01a9..7ef59686 100644 --- a/.gitignore +++ b/.gitignore @@ -68,7 +68,6 @@ monitoring/README.md .DS_Store ### Local agent guidance -/AGENTS.md .claude.md ### Claude Code local settings (machine-specific, not team-shared) diff --git a/CLAUDE.md b/CLAUDE.md index 81377742..b53a9ed6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,15 +3,16 @@ ## Operating constraints ### 자유롭게 해도 되는 것 -- git 작업 자유: 커밋·push·브랜치·머지 등 (소유자 판단). -- 인프라 작업 자유: Docker 배포, 설정 변경, 인프라 리소스. +- 읽기 전용 조사: 파일 읽기, 검색, `git status/diff/log/show`, 영향 범위 분석. +- 로컬 검증: 테스트, 빌드, 정적 검사, dev 환경 확인. +- 브랜치 확인과 커밋 메시지 제안. 실제 커밋, push, merge는 사용자가 명시적으로 요청한 경우에만 수행한다. ### 실사용자 서비스 — 프로덕션 안전 우선 - **라이브 서비스를 깨지 말 것.** 되돌리기 어렵거나 사용자에게 영향 가는 변경(배포·스키마 변경·데이터 마이그레이션)은 실행 전 영향 범위를 설명하고 확인받는다. - **dev 서버에서 먼저 검증 후 prod 반영.** 프로덕션 MySQL 파괴·마이그레이션 신중. - **FCM = 실사용자 푸시 발송.** 발송 경로를 수정하거나 실행하기 전 반드시 경고하고 확인받는다. - 프로덕션 DB에 대한 삭제/대량 수정/truncate는 명시적 확인 없이 실행하지 않는다. -- 되돌리기 어려운/외부로 나가는 작업은 한 번 확인. +- Docker 배포, 인프라 설정 변경, 외부 발송, 되돌리기 어려운 작업은 실행 전 영향 범위와 rollback 방법을 설명하고 확인받는다. ### 작업 방식 - 커밋 메시지 형식: @@ -64,4 +65,4 @@ ``` - 단계 경계에서 사람이 검토 후 다음으로 진행 (자동 연쇄 X). - `analysis`는 요구 명확하면 생략 가능. `explain`은 아무 때나. -- 검토 강도: plan 후 🔴최고 / feat 후 🟠 / check 결과 🟡 / report 🟢. \ No newline at end of file +- 검토 강도: plan 후 🔴최고 / feat 후 🟠 / check 결과 🟡 / report 🟢. From 926f1157fe86553c131b252d371bb8e8b7e59f87 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 4 Jun 2026 23:35:35 +0900 Subject: [PATCH 07/67] =?UTF-8?q?[Chore]=20hooks=20=ED=8C=8C=EC=9D=B4?= =?UTF-8?q?=EC=8D=AC=20=EC=BA=90=EC=8B=9C=20=EB=94=94=EB=A0=89=ED=84=B0?= =?UTF-8?q?=EB=A6=AC=20gitignore=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .claude/hooks/__pycache__/: 바이트코드 캐시는 로컬 생성물이므로 제외 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7ef59686..64e66cda 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,4 @@ monitoring/README.md ### Claude Code local settings (machine-specific, not team-shared) .claude/settings.local.json +.claude/hooks/__pycache__/ From 6a80478691ddae341b15d6197df8b01cd5494863 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 4 Jun 2026 23:35:48 +0900 Subject: [PATCH 08/67] =?UTF-8?q?[Docs]=20OnO=20=EC=97=90=EC=9D=B4?= =?UTF-8?q?=EC=A0=84=ED=8A=B8=20=EA=B3=B5=ED=86=B5=20=EC=A7=80=EC=B9=A8=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AGENTS.md: AI 에이전트 작업 시 따를 공통 규칙 정의 - 사용자·서비스 맥락, 산출물 기준 등 포함 --- AGENTS.md | 194 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..327d3694 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,194 @@ +OnO 작업 에이전트 지침 + +이 문서는 OnO 프로젝트에서 AI 에이전트가 작업할 때 따라야 하는 공통 규칙이다. 사용자는 백엔드 개발에 더 익숙하며, 프론트엔드와 문서화 작업에서 AI 에이전트가 일관된 기준으로 판단하고 산출물을 남기기를 기대한다. + +## 0. 사용자와 서비스 맥락 + +사용자는 OnO 서비스를 운영하는 1인 개발자다. Flutter 애플리케이션 개발도 직접 하고 있으며, 동시에 백엔드 개발자를 희망하는 취업 준비생이다. + +- OnO는 실사용자가 있는 실제 운영 서비스다. +- 배포된 변경사항은 사용자 환경에 바로 영향을 줄 수 있다. +- 모든 판단의 1순위는 실사용자에게 끼칠 영향이다. +- 기능 구현, 버그 수정, 리팩터링, 설정 변경 모두 사용자 데이터, 학습 기록, 인증, 결제, 동기화, 알림, API 호환성에 미칠 영향을 먼저 고려한다. +- 단순히 코드를 작성하는 데서 끝내지 말고, 운영 중인 서비스에 적용해도 안전한지 확인한다. +- 사용자에게 영향을 줄 수 있는 변경은 영향 범위, 배포 리스크, 롤백 또는 대응 방법을 함께 설명한다. + +## 0.1 백엔드 작업 기준 + +사용자는 백엔드 개발자로 성장하는 것을 중요하게 생각한다. 백엔드 작업을 할 때는 단순 구현뿐 아니라, 백엔드 개발자 관점에서의 참고사항, 주의사항, 더 나은 방법을 함께 제안한다. + +- API 계약, DTO, validation, 예외 처리, 트랜잭션 경계, 동시성, 보안, 성능, 로그, 모니터링, 데이터 정합성을 신중히 검토한다. +- 임시로 동작만 맞추는 방식보다 운영 환경에서 설명 가능한 구조를 우선한다. +- 다만 과한 추상화나 미래 확장용 구조는 만들지 않는다. +- 변경이 사용자 데이터나 DB schema에 영향을 주면 migration, 기존 데이터 호환성, 장애 시 복구 가능성을 확인한다. +- 인증, 권한, 개인정보, 결제, 학습 기록처럼 민감한 흐름은 더 보수적으로 검토한다. +- 더 나은 방법이 있으면 이유와 tradeoff를 짧게 설명하고, 필요한 경우 현재 요청 범위 안에서 적용 가능한 최소 변경을 제안한다. +- 백엔드 개발자로서의 참고사항이나 더 나은 방법은 중요한 설계 판단이 있을 때만 설명한다. + +## 0.1.1 DB schema 안전 규칙 + +운영 서비스의 데이터 보존이 최우선이다. DB schema 자동 생성 설정은 매우 보수적으로 다룬다. + +- H2 인메모리 DB가 아닌 환경에서는 `create`, `create-drop`을 절대 사용하지 않는다. +- 로컬, 개발, 테스트, 운영 DB가 MySQL/PostgreSQL 등 실제 파일 또는 서버 기반 DB라면 `create`, `create-drop` 설정을 추가하거나 변경하지 않는다. +- Hibernate/JPA `ddl-auto` 변경은 기존 데이터 삭제 가능성을 먼저 검토한다. +- schema 변경이 필요하면 migration 도구나 명시적인 SQL 변경 이력을 우선 고려한다. +- 테스트에서 `create` 또는 `create-drop`이 필요하다면 H2 인메모리 DB인지 먼저 확인하고, 설정 파일명과 active profile을 함께 검토한다. +- DB 설정 변경 후에는 어떤 profile에 적용되는지, 운영 환경에 전파될 가능성이 없는지 반드시 확인한다. + +## 0.2 테스트 기준 + +백엔드에서는 테스트 코드를 중요하게 본다. 현재 테스트 코드가 충분하지 않더라도, 앞으로 구현하는 기능과 수정 사항은 가능한 한 테스트를 함께 작성하고 그 테스트로 검증한다. + +- 백엔드 기능 구현 시 관련 테스트를 함께 추가하거나 갱신한다. +- 백엔드 테스트는 단위 테스트와 전체 API 통합 테스트를 우선한다. +- 단위 테스트는 핵심 비즈니스 로직, validation, 예외 분기, 데이터 변환을 빠르게 검증하는 데 사용한다. +- 전체 API 통합 테스트는 실제 요청/응답 흐름, 인증/권한, validation 실패, DB 반영, 에러 응답 형식을 검증하는 데 사용한다. +- 버그 수정은 가능하면 실패하는 테스트를 먼저 만들고, 수정 후 통과시키는 흐름을 따른다. +- 테스트가 어려운 구조라면 이유를 설명하고, 최소한 어떤 검증을 대신 수행했는지 남긴다. +- 테스트 범위는 변경 위험도에 맞춘다. +- API 동작 변경은 성공 케이스뿐 아니라 validation 실패, 권한 실패, null/empty 입력, 예외 케이스도 가능한 범위에서 확인한다. +- 테스트가 부실한 영역을 발견하면 임의로 대규모 개선하지 말고, 필요한 개선 방향을 별도 리스크로 언급한다. + +## 1. 기본 원칙 + +**추측하지 말고, 불확실하면 먼저 드러낸다.** + +- 요구사항이 여러 방식으로 해석될 수 있으면 선택지를 짧게 설명하고 확인한다. +- 구현 전에 관련 문서, 코드 흐름, 상태 관리, 라우팅, API 연동 방식을 먼저 확인한다. +- 단순한 해결책이 있으면 먼저 제안한다. +- 요청받은 범위를 넘어서는 기능, 추상화, 설정화, 리팩터링을 임의로 추가하지 않는다. +- API 계약, 응답 필드, null 가능성, 에러 응답 형태가 불명확하면 임의로 단정하지 않는다. + +## 2. 변경 범위 관리 + +**필요한 파일과 필요한 줄만 수정한다.** + +- 요청과 무관한 코드, 주석, 포맷팅, 파일 이동, 이름 변경을 하지 않는다. +- 주변 코드 스타일, 파일 구조, 네이밍, 상태 관리 패턴을 따른다. +- 내 변경으로 생긴 미사용 import, 변수, 함수만 정리한다. +- 기존에 있던 죽은 코드나 이상한 구조는 임의로 삭제하지 말고 필요하면 별도로 언급한다. +- 같은 파일에 사용자의 다른 변경이 있어도 되돌리지 않는다. +- 모든 변경 라인은 사용자의 요청과 직접 연결되어야 한다. + +## 3. 검증 원칙 + +**작업 목표를 검증 가능한 기준으로 바꾸고, 가능한 범위에서 끝까지 확인한다.** + +- 버그 수정은 재현 조건과 수정 후 확인 방법을 함께 정리한다. +- 기능 구현은 사용자 흐름, 빈 상태, 에러 상태, 로딩 상태, 반응형 영향을 함께 확인한다. +- 백엔드 작업은 관련 단위 테스트, 통합 테스트, 빌드 또는 정적 검사를 우선 수행한다. +- 검증을 실행하지 못했다면 이유와 남은 리스크를 명확히 말한다. +- 위험도가 높은 변경은 diff 검토와 실행 검증을 분리해 2중으로 확인한다. + +## 4. 문서 위치 + +백엔드 기능 문서와 PR 초안은 아래 경로를 기준으로 작성한다. + +```text +/Users/ksm/programing/sw_maestro/OnO_BACKEND/backend/docs +``` + +- 기능별로 새 폴더를 만든다. +- 폴더명은 기능을 알아볼 수 있게 작성한다. +- 같은 기능의 구현 명세서와 PR 초안은 같은 폴더에 둔다. +- 다른 에이전트가 이어서 작업할 수 있도록 배경, 범위, 결정 사항, 검증 방법을 문서에 남긴다. + +## 5. 커스텀 명령어 + +아래 명령어는 실제 CLI 명령이 아니라, 사용자가 대화에서 입력하는 작업 모드다. 에이전트는 명령어의 의도를 해석해 필요한 파일 확인, 문서 작성, 구현, 검토를 수행한다. + +### `plan <기능 설명>` + +해당 기능을 어떻게 구현할지에 관한 개요 및 구현 명세서를 markdown으로 작성한다. + +- 코드를 구현하지 않는다. +- 관련 기존 문서와 코드를 먼저 확인한다. +- `/Users/ksm/programing/sw_maestro/OnO_BACKEND/backend/docs` 안에 해당 기능과 관련된 새 폴더를 만든다. +- 새 폴더 안에 markdown 구현 명세서를 작성한다. +- 문서에는 최소한 다음 내용을 포함한다. + - 기능 목적과 사용자 시나리오 + - 구현 범위와 제외 범위 + - 관련 화면, 라우팅, 상태 관리, API 영향 + - 데이터 흐름과 에러 처리 + + - 구현 단계 + - 검증 방법 + - 배포 리스크와 확인이 필요한 질문 +- 이후 `feat` 명령에서 바로 구현을 시작할 수 있을 만큼 구체적으로 작성한다. + +### `feat` + +`plan`으로 작성한 구현 명세서를 바탕으로 기능을 구현한다. + +- 먼저 어떤 구현 명세서를 기준으로 작업할지 확인한다. +- 명세서가 명확하지 않으면 사용자에게 물어본다. +- 명세서의 범위를 벗어나는 기능을 임의로 추가하지 않는다. +- 명세서와 실제 코드가 다르면 실제 코드 기준으로 판단하되, 차이를 사용자에게 설명한다. +- 구현 후 사용자 영향, 반응형 처리, 배포 리스크를 점검한다. +- 가능한 범위에서 포맷팅, 정적 검사, 관련 테스트를 수행한다. +- 완료 후 추천 커밋 단위와 커밋 메시지를 제안한다. +- 필요에 따라 멀티 에이전트를 활용한다. + +프론트엔드와 백엔드 모두 커밋 메시지는 아래 형식으로 통일한다. + +```text +[Feat] 구현 타이틀 - 구현 상세1 구현 상세2 +``` + +예시: + +```text +[Feat] 학습 달력 주간 뷰 추가 - 태블릿 레이아웃 대응 빈 상태 처리 +[Fix] 오답노트 PDF 내보내기 오류 수정 - null 응답 방어 로딩 상태 개선 +``` + +### `pr <커밋 해쉬>` + +사용자가 입력한 커밋을 포함해, 해당 커밋부터 현재까지의 변경사항을 파악한 뒤, PR 제목 및 내용을 markdown으로 작성한다. + +- `<커밋 해쉬>`부터 현재까지의 커밋 내역과 diff를 확인한다. +- 작업 트리에 아직 커밋되지 않은 변경사항이 있으면 함께 확인하고, PR 초안에 반영 여부를 명시한다. +- 아래 PR 템플릿을 반드시 확인한다. + +```text +/Users/ksm/programing/sw_maestro/OnO_BACKEND/backend/.github/PULL_REQUEST_TEMPLATE.md +``` + +- PR 제목과 본문은 템플릿 양식에 맞춰 작성한다. +- PR markdown 파일은 `plan`에서 만든 기능 문서 폴더 안에, 구현 명세서와 같은 위치에 저장한다. +- 어떤 기능 문서 폴더를 사용해야 할지 명확하지 않으면 사용자에게 확인한다. +- PR 본문에는 변경사항 요약, 사용자 영향, 검증 결과, 배포 리스크를 포함한다. + +### `check <커밋 번호>` + +해당 커밋부터 현재까지의 변경사항을 모두 분석한 뒤, 위험한 사항이나 에러 가능성이 없는지 검증한다. + +- `<커밋 번호>`가 있으면 해당 커밋부터 현재까지의 변경사항을 확인한다. +- 커밋 번호가 없으면 현재 작업 중인 변경사항만 검토한다. +- 프로젝트 전체를 대충 훑지 말고, 변경된 파일과 영향받는 사용자 흐름을 기준으로 검토한다. +- 검사는 반드시 2중으로 수행한다. +- 1차 검사는 diff 기반으로 수행한다. + - 변경된 코드의 의도와 실제 동작이 맞는지 확인한다. + - API 연동, 상태 관리, null 처리, 에러 처리, 로딩 상태, 데이터 손실 가능성을 확인한다. + - UI 변경이 있으면 폰과 태블릿에서 overflow, 잘림, 스크롤 불가 가능성을 확인한다. +- 2차 검사는 실행 가능한 검증으로 수행한다. + - 가능한 경우 포맷팅 검사, 정적 분석, 관련 테스트, 빌드 영향 파일 확인을 진행한다. + - 실행 검증이 어렵다면 구조 검토로 대체하고, 대체한 이유를 적는다. +- 검토 결과는 위험도 순서로 정리한다. +- 문제를 발견하면 파일과 라인 근거를 포함한다. +- 발견한 문제가 없더라도 남은 리스크와 실행하지 못한 검증을 명확히 적는다. + +### `commit` + +- 현재 변경사항에 대해 커밋 단위 및 커밋 메시지를 양식에 맞게 추천한다. + +## 6. 작업 보고 + +작업 완료 후에는 다음 내용을 간결하게 정리한다. + +- 변경한 내용 +- 사용자에게 미치는 영향 +- 실행한 검증과 결과 +- 남은 리스크 또는 확인이 필요한 부분 +- 추천 커밋 단위와 커밋 메시지 From 78b4e5aad279f5c0450499570896219d07c6567d Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 4 Jun 2026 23:35:55 +0900 Subject: [PATCH 09/67] =?UTF-8?q?[Feat]=20pr,=20migrate=20=EC=8A=A4?= =?UTF-8?q?=ED=82=AC=20=EC=B6=94=EA=B0=80=20(Claude=20Code=20/=20Codex)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .claude/commands/pr.md: 기준 커밋~현재 변경사항으로 PR 초안 작성 커맨드 - .codex/skills/pr/: Codex pr 스킬 정의 - .codex/skills/migrate/: Flyway 마이그레이션 파일 작성 및 안전성 검토 스킬 --- .claude/commands/pr.md | 17 +++++++++++ .codex/skills/migrate/SKILL.md | 36 ++++++++++++++++++++++++ .codex/skills/migrate/agents/openai.yaml | 7 +++++ .codex/skills/pr/SKILL.md | 25 ++++++++++++++++ .codex/skills/pr/agents/openai.yaml | 7 +++++ 5 files changed, 92 insertions(+) create mode 100644 .claude/commands/pr.md create mode 100644 .codex/skills/migrate/SKILL.md create mode 100644 .codex/skills/migrate/agents/openai.yaml create mode 100644 .codex/skills/pr/SKILL.md create mode 100644 .codex/skills/pr/agents/openai.yaml diff --git a/.claude/commands/pr.md b/.claude/commands/pr.md new file mode 100644 index 00000000..3a3c12cb --- /dev/null +++ b/.claude/commands/pr.md @@ -0,0 +1,17 @@ +--- +description: PR 제목과 본문 초안 작성. 기준 커밋부터 현재까지의 변경사항을 PR 템플릿에 맞춰 정리. +argument-hint: "<기준 커밋 해시>" +--- + +# /pr — PR 초안 + +## 기준: $ARGUMENTS + +## 단계 + +1. **변경 범위 확인** — `<커밋>..HEAD`의 커밋 로그와 diff를 확인한다. 인자가 없으면 현재 작업 트리 변경사항을 대상으로 한다. +2. **미커밋 변경 확인** — `git status`로 아직 커밋되지 않은 변경사항을 확인하고, PR 본문 반영 여부를 명시한다. +3. **템플릿 확인** — `.github/PULL_REQUEST_TEMPLATE.md`를 읽고 같은 구조로 작성한다. +4. **영향 범위 정리** — 사용자 영향, API 호환성, DB migration, 인증/권한, 알림/외부 발송, 배포 리스크를 확인한다. +5. **검증 결과 정리** — 실행한 테스트/빌드 명령과 결과, 실행하지 못한 검증과 남은 리스크를 적는다. +6. **저장 위치 확인** — 기능 문서 폴더가 명확하면 같은 위치에 PR markdown을 저장하고, 불명확하면 사용자에게 확인한다. diff --git a/.codex/skills/migrate/SKILL.md b/.codex/skills/migrate/SKILL.md new file mode 100644 index 00000000..2489cee5 --- /dev/null +++ b/.codex/skills/migrate/SKILL.md @@ -0,0 +1,36 @@ +--- +name: migrate +description: OnO DB schema 변경과 Flyway 마이그레이션 파일 작성/검토가 필요할 때 사용. 사용자가 "migrate <스키마 변경>"를 요청하면 기존 migration, entity, query 영향을 확인하고 안전한 SQL 초안을 만든다. +--- + +# OnO Migrate + +## Workflow + +1. 스키마 변경 목적, 대상 테이블, 사용자 데이터 영향 범위를 먼저 확인한다. +2. `src/main/resources/db/migration/`의 최신 Flyway 버전 번호와 기존 변경 이력을 확인한다. +3. 관련 entity, repository, query, DTO/API 계약을 추적한다. +4. 운영 DB 기준으로 되돌리기 어려운 작업인지 확인한다. +5. 마이그레이션은 가능한 안전한 순서로 설계한다. + - `ADD COLUMN NULL` + - 필요한 경우 backfill `UPDATE` + - 검증 후 `NOT NULL`, index, constraint 적용 +6. 대용량 테이블이면 lock, full scan, 배포 시간, rollback 가능성을 명시한다. +7. SQL 파일은 `V{N+1}__.sql` 형식으로 제안한다. +8. 파일 저장 전에는 사용자에게 최종 확인한다. 승인 없이 production 영향 가능성이 있는 migration 파일을 확정하지 않는다. + +## Guardrails + +- H2 인메모리 DB가 아닌 환경에 `ddl-auto=create` 또는 `create-drop`을 추가하지 않는다. +- `DROP`, `TRUNCATE`, column type 축소, `NOT NULL` 즉시 추가, 대량 backfill은 별도 위험으로 표시한다. +- rollback SQL 또는 대응 절차를 주석이나 보고에 포함한다. +- API 응답/요청 구조가 함께 바뀌면 Flutter 호환성과 기존 앱 버전 영향을 확인한다. + +## Output + +- 변경 목적과 대상 schema +- 기존 코드/쿼리 영향 +- migration SQL 초안 또는 생성 파일 +- rollback/대응 방법 +- dev/prod 적용 전 검증 방법 +- 배포 리스크와 확인 질문 diff --git a/.codex/skills/migrate/agents/openai.yaml b/.codex/skills/migrate/agents/openai.yaml new file mode 100644 index 00000000..475551cc --- /dev/null +++ b/.codex/skills/migrate/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "OnO Migrate" + short_description: "OnO DB 마이그레이션 안전 워크플로우" + default_prompt: "Use $migrate to design and review this OnO database migration." + +policy: + allow_implicit_invocation: true diff --git a/.codex/skills/pr/SKILL.md b/.codex/skills/pr/SKILL.md new file mode 100644 index 00000000..e7efc8c4 --- /dev/null +++ b/.codex/skills/pr/SKILL.md @@ -0,0 +1,25 @@ +--- +name: pr +description: OnO PR 제목과 본문 초안을 작성할 때 사용. 사용자가 "pr <커밋 해시>"를 요청하면 해당 커밋부터 현재까지의 변경과 미커밋 변경을 확인해 PR 템플릿에 맞춰 markdown을 작성한다. +--- + +# OnO PR + +## Workflow + +1. 기준 커밋 해시 또는 변경 범위를 확인한다. 인자가 없으면 현재 git 변경사항을 대상으로 한다. +2. `git log`, `git diff`, `git status`로 커밋된 변경과 미커밋 변경을 분리한다. +3. `.github/PULL_REQUEST_TEMPLATE.md`를 반드시 읽고 같은 구조로 본문을 작성한다. +4. 수정된 파일, API/DB/인증/권한/알림/배포 영향 범위를 정리한다. +5. 테스트와 검증은 실행 여부, 명령, 결과를 구분해 적는다. +6. 저장 위치가 명확하지 않으면 사용자에게 확인한다. 기능 문서가 있으면 같은 `docs/<기능폴더>` 아래에 저장한다. +7. 미커밋 변경사항은 PR 본문에 포함했는지 여부를 명시한다. + +## Output + +- PR 제목 +- PR 본문 markdown +- 포함한 커밋/미커밋 변경 범위 +- 사용자 영향 +- 검증 결과 +- 배포 리스크와 rollback/대응 방법 diff --git a/.codex/skills/pr/agents/openai.yaml b/.codex/skills/pr/agents/openai.yaml new file mode 100644 index 00000000..04aaf8aa --- /dev/null +++ b/.codex/skills/pr/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "OnO PR" + short_description: "OnO PR 초안 작성 워크플로우" + default_prompt: "Use $pr to draft this OnO pull request." + +policy: + allow_implicit_invocation: true From 76fd8ca1a392c91f6c49689b44fd554b377a115f Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 4 Jun 2026 23:36:07 +0900 Subject: [PATCH 10/67] =?UTF-8?q?[Chore]=20=EC=97=90=EC=9D=B4=EC=A0=84?= =?UTF-8?q?=ED=8A=B8=20=EC=84=A4=EC=A0=95=20=ED=8C=8C=EC=9D=BC=20lint=20CI?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .codex/scripts/agent-config-lint.sh: 에이전트 설정 파일 유효성 검사 스크립트 - .github/workflows/agent-config-lint.yml: PR 시 에이전트 설정 변경 자동 검사 워크플로 --- .codex/scripts/agent-config-lint.sh | 74 +++++++++++++++++++++++++ .github/workflows/agent-config-lint.yml | 36 ++++++++++++ 2 files changed, 110 insertions(+) create mode 100755 .codex/scripts/agent-config-lint.sh create mode 100644 .github/workflows/agent-config-lint.yml diff --git a/.codex/scripts/agent-config-lint.sh b/.codex/scripts/agent-config-lint.sh new file mode 100755 index 00000000..8c9c03d9 --- /dev/null +++ b/.codex/scripts/agent-config-lint.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -euo pipefail + +fail=0 + +error() { + printf 'ERROR: %s\n' "$1" >&2 + fail=1 +} + +require_file() { + if [ ! -f "$1" ]; then + error "missing required file: $1" + fi +} + +primary_commands=( + analysis + check + commit + explain + feat + migrate + perf + plan + pr + report + test +) + +for command in "${primary_commands[@]}"; do + require_file ".claude/commands/${command}.md" + require_file ".codex/skills/${command}/SKILL.md" + require_file ".codex/skills/${command}/agents/openai.yaml" +done + +require_file ".codex/skills/perf-audit/SKILL.md" +require_file ".codex/skills/test-writer/SKILL.md" + +for required in "사용자 영향" "API 호환성" "DB migration" "인증/권한" "검증 결과" "배포 리스크" "롤백"; do + if ! grep -q "$required" .github/PULL_REQUEST_TEMPLATE.md; then + error "PR template is missing section text: $required" + fi +done + +if grep -q '^/AGENTS\.md$' .gitignore; then + error "AGENTS.md is ignored; shared agent guidance should be trackable" +fi + +tracked_secret_files="$( + git ls-files | grep -E '(^|/)(\.env(\..*)?|FirebaseAdminKey\.json|application(-(dev|prod|local|test))?\.ya?ml|application(-(dev|prod|local|test))?\.properties)$' || true +)" + +if [ -n "$tracked_secret_files" ]; then + printf '%s\n' "$tracked_secret_files" >&2 + error "secret/config files are tracked" +fi + +secret_hits="$( + git grep -n -E -- \ + '-----BEGIN (RSA |EC |OPENSSH |PRIVATE )?PRIVATE KEY-----|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}' \ + -- ':!*.md' ':!.codex/scripts/agent-config-lint.sh' || true +)" + +if [ -n "$secret_hits" ]; then + printf '%s\n' "$secret_hits" >&2 + error "possible secret material found in tracked files" +fi + +if [ "$fail" -ne 0 ]; then + exit 1 +fi + +printf 'Agent configuration lint passed.\n' diff --git a/.github/workflows/agent-config-lint.yml b/.github/workflows/agent-config-lint.yml new file mode 100644 index 00000000..27094e4b --- /dev/null +++ b/.github/workflows/agent-config-lint.yml @@ -0,0 +1,36 @@ +name: Agent Config Lint + +on: + pull_request: + paths: + - "AGENTS.md" + - "CLAUDE.md" + - ".claude/**" + - ".codex/**" + - ".github/PULL_REQUEST_TEMPLATE.md" + - ".github/workflows/agent-config-lint.yml" + - ".gitignore" + push: + branches: ["main", "develop"] + paths: + - "AGENTS.md" + - "CLAUDE.md" + - ".claude/**" + - ".codex/**" + - ".github/PULL_REQUEST_TEMPLATE.md" + - ".github/workflows/agent-config-lint.yml" + - ".gitignore" + +permissions: + contents: read + +jobs: + agent-config-lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run agent config lint + run: bash .codex/scripts/agent-config-lint.sh From 0ea71339c2f09c4ca3e4ceefb8df8040fa9b130f Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sat, 6 Jun 2026 10:58:02 +0900 Subject: [PATCH 11/67] =?UTF-8?q?[Chore]=20/pr=20=EB=AA=85=EB=A0=B9?= =?UTF-8?q?=EC=96=B4=EB=A5=BC=20GitHub=20PR=20=EC=A7=81=EC=A0=91=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/commands/pr.md | 143 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 133 insertions(+), 10 deletions(-) diff --git a/.claude/commands/pr.md b/.claude/commands/pr.md index 3a3c12cb..7a93a813 100644 --- a/.claude/commands/pr.md +++ b/.claude/commands/pr.md @@ -1,17 +1,140 @@ --- -description: PR 제목과 본문 초안 작성. 기준 커밋부터 현재까지의 변경사항을 PR 템플릿에 맞춰 정리. -argument-hint: "<기준 커밋 해시>" +description: 현재 브랜치의 개발 내역을 develop 브랜치로 merge 요청하는 GitHub PR 생성. +argument-hint: "<추가 설명 (선택)>" --- -# /pr — PR 초안 +# /pr — GitHub PR 생성 (→ develop) -## 기준: $ARGUMENTS +## 추가 설명: $ARGUMENTS + +## 주의사항 + +> ⚠️ **main 브랜치로는 절대 PR을 생성하지 않는다.** main merge는 프로덕션 자동 배포로 이어지므로 항상 사용자가 직접 수행한다. +> 이 명령어는 현재 브랜치 → `develop` PR만 생성한다. + +--- ## 단계 -1. **변경 범위 확인** — `<커밋>..HEAD`의 커밋 로그와 diff를 확인한다. 인자가 없으면 현재 작업 트리 변경사항을 대상으로 한다. -2. **미커밋 변경 확인** — `git status`로 아직 커밋되지 않은 변경사항을 확인하고, PR 본문 반영 여부를 명시한다. -3. **템플릿 확인** — `.github/PULL_REQUEST_TEMPLATE.md`를 읽고 같은 구조로 작성한다. -4. **영향 범위 정리** — 사용자 영향, API 호환성, DB migration, 인증/권한, 알림/외부 발송, 배포 리스크를 확인한다. -5. **검증 결과 정리** — 실행한 테스트/빌드 명령과 결과, 실행하지 못한 검증과 남은 리스크를 적는다. -6. **저장 위치 확인** — 기능 문서 폴더가 명확하면 같은 위치에 PR markdown을 저장하고, 불명확하면 사용자에게 확인한다. +### 1. 브랜치 상태 확인 +- `git branch --show-current`로 현재 브랜치 이름 확인 +- 현재 브랜치가 `main` 또는 `develop`이면 **즉시 중단**하고 작업 브랜치에서 실행해야 한다고 알린다 +- `git log develop..HEAD --oneline`으로 develop 대비 커밋 목록 확인 +- `git diff develop...HEAD`로 변경 내용 파악 +- `git status`로 미커밋 변경사항 확인 (있으면 사용자에게 명시) +- 원격에 push 여부 확인 (`git log origin/$(git branch --show-current)..HEAD 2>/dev/null`로 미push 커밋 확인) + +### 2. 변경 내용 분석 +커밋 로그와 diff를 바탕으로 다음을 파악한다: +- **작업 유형**: feat / fix / refactor / chore / perf / docs / test (복수 가능) +- **영향 범위**: 어떤 패키지·API·DB 스키마가 바뀌었는지 +- **연관 이슈 후보 추출**: 커밋 메시지와 브랜치명에서 이슈 번호 또는 키워드를 추출한다 + - 브랜치명 패턴: `feat/123-login` → 번호 `123`, 키워드 `login` + - 커밋 메시지 패턴: `close #123`, `fix #123`, `resolve #123` → 번호 `123` + - 번호가 없으면 작업 내용을 요약한 키워드를 추출한다 + +### 2-1. GitHub에서 연관 이슈 검색 +`mcp__github__.list_issues`를 사용해 `AI-SIP/OnO_BACKEND` 리포지토리의 **open 상태** 이슈를 검색한다: +- 2단계에서 추출한 이슈 번호나 키워드를 open issue의 번호, 제목, 본문과 비교한다 +- 검색 결과 중 이번 PR 작업과 연관성이 높은 이슈를 골라 연관 이슈 목록을 확정한다 +- PR에서 실제로 해결되는 이슈만 `close #이슈번호`로 연결한다 +- 단순 참고 이슈는 자동 종료되지 않도록 `close` 없이 참고 문장으로만 작성한다 +- 연관 이슈를 찾지 못하면 "없음"으로 처리한다 + +### 3. 레이블 결정 +변경 내용을 분석해 다음 중 하나 이상으로 레이블을 결정한다: + +| 작업 유형 | label | +|---|---| +| 신규 기능 | `feature` | +| 버그 수정 | `bug` | +| 리팩터링 | `refactor` | +| 설정·의존성·chore | `chore` | +| 성능 개선 | `performance` | +| 문서·주석 | `documentation` | +| 테스트 | `test` | + +결정한 label은 적용 전에 `mcp__github__.get_label`로 존재 여부를 확인하고, 존재하는 label만 적용한다. + +### 4. PR 제목 결정 +브랜치명과 커밋 내역을 종합해 PR 제목을 결정한다. +- 형식: `[타입] 핵심 변경 요약` (예: `[Feat] 오답노트 태그 필터 기능 추가`) +- 여러 타입이 섞이면 가장 비중이 큰 타입 하나를 사용 + +### 5. PR 본문 작성 +`.github/PULL_REQUEST_TEMPLATE.md` 구조를 그대로 따른다. 실제 변경 내용에 맞게 체크박스를 채운다: + +``` +## ✔️ 연관 이슈 +- close #이슈번호 ← 2-1단계에서 찾았고 PR merge 시 닫아야 하는 이슈마다 한 줄씩 기재 +- 없음 ← 관련 이슈가 없으면 기재 + +## 📝 작업 내용 +> [커밋 단위로 bullet 정리] + +## 👤 사용자 영향 +- [사용자 관점에서 달라지는 점 / 영향 없으면 "없음"] + +## 🔌 API 호환성 +- [ ] 요청/응답 DTO 변경 없음 +- [ ] 기존 Flutter 앱 버전과 호환 확인 +- [ ] breaking change 있음 + +## 🗄️ DB migration +- [ ] 없음 +- [ ] 있음: migration 파일과 기존 데이터 호환성 확인 + +## 🔐 인증/권한 +- [ ] 영향 없음 +- [ ] 사용자별 데이터 소유권 검증 확인 +- [ ] 관리자/특수 권한 영향 확인 + +## ✅ 검증 결과 +- [실행한 테스트·빌드 명령과 결과 / 실행하지 못한 검증과 남은 리스크] + +## 🚀 배포 리스크 +- [배포 시 주의할 점 / 없으면 "없음"] + +## ↩️ 롤백/대응 방법 +- [문제 발생 시 롤백 방법 / 해당 없으면 "없음"] + +### 스크린샷 (선택) +``` + +### 6. 생성 전 미리보기 확인 +PR을 생성하기 전에 다음 항목을 사용자에게 보여주고 확인을 받는다: +- **Base → Head**: `develop` ← 현재 브랜치명 +- **제목**: 결정된 제목 +- **Labels**: 결정된 레이블 +- **연관 이슈**: `close #이슈번호` 또는 없음 +- **본문 미리보기**: 작성된 본문 전체 + +사용자가 수정을 요청하면 반영한 뒤 재확인한다. 승인하면 다음 단계로 진행한다. + +### 7. 원격 브랜치 push 확인 +미push 커밋이 있으면 사용자에게 알리고, push를 완료한 뒤 진행할 것을 안내한다. +push가 완료된 것을 확인한 후 PR을 생성한다. + +### 8. GitHub PR 생성 +`mcp__github__.create_pull_request` 도구를 사용해 PR을 생성한다: +- `owner`: `AI-SIP` +- `repo`: `OnO_BACKEND` +- `title`: 4단계에서 결정한 제목 +- `body`: 5단계에서 작성한 본문 +- `head`: 현재 브랜치명 +- `base`: `develop` (절대 고정 — main 불가) + +Reviewers는 지정하지 않는다. + +### 8-1. Assignee/Label 설정 +PR 생성 후 생성된 PR 번호에 `mcp__github__.issue_write` 도구를 `method=update`로 호출한다: +- `owner`: `AI-SIP` +- `repo`: `OnO_BACKEND` +- `issue_number`: 생성된 PR 번호 +- `assignees`: `["KiSeungMin"]` (항상 고정) +- `labels`: 3단계에서 결정하고 존재 확인된 label 배열 + +milestone, project, issue fields, type 등 나머지 항목은 지정하지 않는다. + +### 9. 결과 보고 +생성된 PR URL, base/head branch, assignee, 적용 label, 연결한 issue, `main`을 target으로 사용하지 않았다는 점을 사용자에게 알린다. From 207c6e7cfd4d648019068249c537e13bb3142157 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sat, 6 Jun 2026 10:58:10 +0900 Subject: [PATCH 12/67] =?UTF-8?q?[Feat]=20/issue=20=EB=AA=85=EB=A0=B9?= =?UTF-8?q?=EC=96=B4=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20develop=20?= =?UTF-8?q?=EB=A8=B8=EC=A7=80=20=EC=8B=9C=20=EC=9D=B4=EC=8A=88=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EC=A2=85=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/commands/issue.md | 83 +++++++++++++++++++ .../close-issues-on-develop-merge.yml | 51 ++++++++++++ AGENTS.md | 60 ++++++++++++-- 3 files changed, 185 insertions(+), 9 deletions(-) create mode 100644 .claude/commands/issue.md create mode 100644 .github/workflows/close-issues-on-develop-merge.yml diff --git a/.claude/commands/issue.md b/.claude/commands/issue.md new file mode 100644 index 00000000..b4c56452 --- /dev/null +++ b/.claude/commands/issue.md @@ -0,0 +1,83 @@ +--- +description: AI-SIP/OnO_BACKEND 리포지토리에 GitHub 이슈를 생성한다. 텍스트 설명 또는 파일 경로를 받아 템플릿에 맞게 이슈를 작성한다. +argument-hint: "<이슈 설명 또는 파일 경로>" +--- + +# /issue — GitHub 이슈 생성 + +## 입력: $ARGUMENTS + +## 단계 + +### 1. 입력 파악 +- `$ARGUMENTS`가 존재하는 파일 경로이면 해당 파일을 읽어 내용을 이슈 소스로 사용한다. +- 파일 경로가 아니면 텍스트 그대로를 이슈 소스로 사용한다. +- 인자가 없으면 사용자에게 이슈 내용을 물어본다. + +### 2. 이슈 유형 판단 +입력 내용을 분석해 다음 중 하나로 분류한다: + +| 유형 | 사용 템플릿 | labels | 제목 prefix | +|---|---|---|---| +| 버그·오류·에러·crash·NPE | Bug Report Template | `bug` | `[bug]` | +| 기능 추가·신규 개발 | Common Issue Template | `feature` | `[feat]` | +| 리팩터·코드 정리 | Common Issue Template | `refactor` | `[refactor]` | +| 설정·환경·의존성·chore | Common Issue Template | `chore` | `[chore]` | +| 성능 개선 | Common Issue Template | `performance` | `[perf]` | +| 문서·주석 | Common Issue Template | `documentation` | `[docs]` | +| 테스트 | Common Issue Template | `test` | `[test]` | +| 기타·불명확 | Common Issue Template | (없음) | `[chore]` | + +### 3. 템플릿 채우기 + +**Bug Report Template** (버그 유형): +``` +## ⚙️ 어떤 버그인가요? +[버그 핵심 요약 — 1~2문장] + +### 스크린샷(선택) +[입력에 첨부 이미지·로그가 있으면 삽입, 없으면 섹션 생략] + +## 🔎 어떤 상황에서 발생한 버그인가요? +**Given** — [사전 조건] +**When** — [행동] +**Then** — [실제 결과 / 오류] + +## ✅ 예상 결과 +[정상적으로 동작해야 하는 결과] +``` + +**Common Issue Template** (그 외): +``` +## 📝 Description +[작업에 대한 간략한 설명] + +## ✅ TODO +- [ ] [할 일 1] +- [ ] [할 일 2] +(입력에서 구체적인 작업 항목을 추출해 체크리스트로 작성) + +## 💡 ETC (선택) +[기타 참고 사항, 고려할 점, 논의 필요 사항 — 없으면 섹션 생략] +``` + +### 4. 이슈 생성 전 미리보기 확인 +이슈를 생성하기 전에 다음 항목을 사용자에게 보여주고 확인한다: +- **제목**: 결정된 제목 +- **Labels**: 결정된 레이블 +- **본문 미리보기**: 작성된 본문 전체 + +사용자가 수정을 요청하면 반영한 뒤 재확인한다. 승인하면 5단계로 진행한다. + +### 5. GitHub 이슈 생성 +`mcp__github__issue_write` 도구를 사용해 이슈를 생성한다: +- `owner`: `AI-SIP` +- `repo`: `OnO_BACKEND` +- `method`: `create` +- `title`: 3단계에서 결정한 제목 +- `body`: 3단계에서 작성한 본문 +- `assignees`: `["KiSeungMin"]` (항상 고정) +- `labels`: 2단계에서 결정한 레이블 배열 + +### 6. 결과 보고 +생성된 이슈 URL을 사용자에게 알린다. \ No newline at end of file diff --git a/.github/workflows/close-issues-on-develop-merge.yml b/.github/workflows/close-issues-on-develop-merge.yml new file mode 100644 index 00000000..6d3c908f --- /dev/null +++ b/.github/workflows/close-issues-on-develop-merge.yml @@ -0,0 +1,51 @@ +name: Close Issues on Develop Merge + +on: + pull_request: + types: [closed] + branches: + - develop + +permissions: + issues: write + +jobs: + close-related-issues: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + + steps: + - name: Close referenced issues + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const body = context.payload.pull_request.body || ''; + const pattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi; + const issueNumbers = []; + let match; + + while ((match = pattern.exec(body)) !== null) { + issueNumbers.push(parseInt(match[1], 10)); + } + + const unique = [...new Set(issueNumbers)]; + if (unique.length === 0) { + console.log('연관 이슈 없음 — 종료'); + return; + } + + for (const number of unique) { + try { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: number, + state: 'closed', + state_reason: 'completed', + }); + console.log(`이슈 #${number} 닫힘`); + } catch (e) { + console.log(`이슈 #${number} 처리 실패: ${e.message}`); + } + } \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 327d3694..cdba2456 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -143,22 +143,64 @@ OnO 작업 에이전트 지침 [Fix] 오답노트 PDF 내보내기 오류 수정 - null 응답 방어 로딩 상태 개선 ``` -### `pr <커밋 해쉬>` - -사용자가 입력한 커밋을 포함해, 해당 커밋부터 현재까지의 변경사항을 파악한 뒤, PR 제목 및 내용을 markdown으로 작성한다. - -- `<커밋 해쉬>`부터 현재까지의 커밋 내역과 diff를 확인한다. -- 작업 트리에 아직 커밋되지 않은 변경사항이 있으면 함께 확인하고, PR 초안에 반영 여부를 명시한다. +### `pr` + +현재 브랜치의 개발 내역을 바탕으로 `AI-SIP/OnO_BACKEND` GitHub Repository에 `develop` 브랜치로 merge 요청하는 Pull Request를 직접 생성한다. + +- `codex_apps` GitHub 도구가 아니라 `mcp__github__` MCP 서버의 `create_pull_request` 도구를 사용한다. +- Repository는 항상 `owner=AI-SIP`, `repo=OnO_BACKEND`를 사용한다. +- base branch는 항상 `develop`으로 지정한다. +- `main` branch를 base로 하는 PR은 절대 생성하지 않는다. `main` merge와 production 배포는 항상 사용자가 직접 담당한다. +- 현재 브랜치가 `main` 또는 `develop`이면 PR을 생성하지 말고, 작업 브랜치에서 실행해야 한다고 보고한다. +- 현재 브랜치 이름과 원격 브랜치 존재 여부를 확인한다. +- 작업 트리에 아직 커밋되지 않은 변경사항이 있으면 PR을 생성하지 말고, 먼저 커밋 또는 정리가 필요하다고 보고한다. +- 현재 브랜치가 원격에 push되어 있지 않으면 PR을 생성할 수 없으므로 사용자에게 push 필요 여부를 확인한다. +- `develop..현재브랜치`의 커밋 로그와 diff를 확인해 PR 제목과 본문을 작성한다. +- PR 생성 전에 GitHub에서 open issue를 검색해 현재 변경사항과 관련된 이슈가 있는지 확인한다. +- 관련 이슈가 있으면 PR 본문 `연관 이슈` 항목에 `close #이슈번호`를 포함해 merge 시 자동 종료되도록 작성한다. +- 관련 이슈가 없으면 `연관 이슈` 항목에는 관련 이슈 없음이라고 명시한다. - 아래 PR 템플릿을 반드시 확인한다. ```text /Users/ksm/programing/sw_maestro/OnO_BACKEND/backend/.github/PULL_REQUEST_TEMPLATE.md ``` -- PR 제목과 본문은 템플릿 양식에 맞춰 작성한다. -- PR markdown 파일은 `plan`에서 만든 기능 문서 폴더 안에, 구현 명세서와 같은 위치에 저장한다. -- 어떤 기능 문서 폴더를 사용해야 할지 명확하지 않으면 사용자에게 확인한다. +- PR 제목과 본문은 템플릿 양식에 맞춰 작성하고, PR markdown 파일은 별도로 만들지 않는다. - PR 본문에는 변경사항 요약, 사용자 영향, 검증 결과, 배포 리스크를 포함한다. +- Reviewers는 지정하지 않는다. +- PR 생성 후 생성된 PR 번호에 `mcp__github__` MCP 서버의 `issue_write` 도구를 `method=update`로 호출해 Assignees와 Labels를 설정한다. +- Assignees는 항상 `KiSeungMin`으로 지정한다. +- Labels는 변경 성격에 맞게 `feat`, `fix`, `refactor`, `chore`, `docs`, `test`, `bug` 중 적절한 후보를 고르되, `get_label`로 존재 여부를 확인한 label만 적용한다. +- milestone, project, issue fields, type 등 나머지 항목은 지정하지 않는다. +- PR 생성 후에는 PR 번호, URL, base/head branch, assignee, 적용한 label, main을 target으로 사용하지 않았다는 점을 간결하게 보고한다. + +### `/issue <내용 또는 파일 경로>` + +사용자가 자유롭게 작성한 내용이나 파일 내용을 바탕으로 `AI-SIP/OnO_BACKEND` GitHub Repository에 이슈를 생성한다. + +- `codex_apps` GitHub 도구가 아니라 `mcp__github__` MCP 서버의 `issue_write` 도구를 사용한다. +- Repository는 항상 `owner=AI-SIP`, `repo=OnO_BACKEND`를 사용한다. +- Assignees는 항상 `KiSeungMin`으로 지정한다. +- 사용자가 파일 경로를 전달하면 먼저 해당 파일을 읽고, 파일 내용을 이슈 원문으로 사용한다. +- 사용자가 직접 내용을 입력하면 그 내용을 이슈 원문으로 사용한다. +- 원문이 부족해도 가능한 범위에서 이슈를 생성하되, 운영 영향이나 재현 조건처럼 필수 판단에 필요한 정보가 없으면 이슈 본문에 "확인 필요"로 명시한다. +- 아래 이슈 템플릿을 반드시 확인하고, 입력 성격에 맞는 템플릿을 선택해 본문을 채운다. + +```text +/Users/ksm/programing/sw_maestro/OnO_BACKEND/backend/.github/ISSUE_TEMPLATE/bug-report-template.md +/Users/ksm/programing/sw_maestro/OnO_BACKEND/backend/.github/ISSUE_TEMPLATE/common-issue-template.md +``` + +- 버그, 오류, 예외, 장애, 잘못된 동작, 회귀 이슈는 `bug-report-template.md` 형식을 사용한다. + - 제목은 `[bug] ...` 형식으로 작성한다. + - label은 `bug`를 포함한다. + - 본문에는 버그 설명, 발생 상황, 예상 결과를 템플릿 항목에 맞춰 정리한다. +- 기능 추가, 개선, 리팩터링, 문서, 설정, 운영 작업은 `common-issue-template.md` 형식을 사용한다. + - 제목은 `[feat]`, `[fix]`, `[refactor]`, `[chore]`, `[docs]`, `[test]` 중 가장 적절한 타입을 골라 작성한다. + - label은 선택한 타입과 이슈 성격에 맞게 적용하되, 존재 여부가 불확실한 label은 무리하게 붙이지 않는다. + - 본문에는 Description, TODO, ETC를 템플릿 항목에 맞춰 정리한다. +- GitHub issue type 기능이 사용 가능하면 입력 성격에 맞는 type을 설정한다. 유효한 type을 확인할 수 없거나 Repository가 issue type을 지원하지 않으면 `type` 필드는 생략한다. +- 이슈 생성 후에는 이슈 번호, URL, 적용한 assignee, label, type을 사용자에게 간결하게 보고한다. ### `check <커밋 번호>` From c638451bb4fb29197257b5fd9d146f916cd6dacf Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sat, 6 Jun 2026 11:18:36 +0900 Subject: [PATCH 13/67] =?UTF-8?q?[Chore]=20Codex=20/pr=20=EC=8A=A4?= =?UTF-8?q?=ED=82=AC=EC=9D=84=20GitHub=20PR=20=EC=A7=81=EC=A0=91=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .codex/skills/pr/SKILL.md | 49 ++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/.codex/skills/pr/SKILL.md b/.codex/skills/pr/SKILL.md index e7efc8c4..7a3e7934 100644 --- a/.codex/skills/pr/SKILL.md +++ b/.codex/skills/pr/SKILL.md @@ -1,25 +1,52 @@ --- name: pr -description: OnO PR 제목과 본문 초안을 작성할 때 사용. 사용자가 "pr <커밋 해시>"를 요청하면 해당 커밋부터 현재까지의 변경과 미커밋 변경을 확인해 PR 템플릿에 맞춰 markdown을 작성한다. +description: OnO 현재 브랜치의 개발 내역을 바탕으로 develop 브랜치에 merge 요청하는 GitHub PR을 직접 생성할 때 사용. 사용자가 "pr"을 요청하면 현재 브랜치에서 develop으로 PR을 생성한다. --- # OnO PR ## Workflow -1. 기준 커밋 해시 또는 변경 범위를 확인한다. 인자가 없으면 현재 git 변경사항을 대상으로 한다. -2. `git log`, `git diff`, `git status`로 커밋된 변경과 미커밋 변경을 분리한다. -3. `.github/PULL_REQUEST_TEMPLATE.md`를 반드시 읽고 같은 구조로 본문을 작성한다. -4. 수정된 파일, API/DB/인증/권한/알림/배포 영향 범위를 정리한다. -5. 테스트와 검증은 실행 여부, 명령, 결과를 구분해 적는다. -6. 저장 위치가 명확하지 않으면 사용자에게 확인한다. 기능 문서가 있으면 같은 `docs/<기능폴더>` 아래에 저장한다. -7. 미커밋 변경사항은 PR 본문에 포함했는지 여부를 명시한다. +1. 현재 브랜치를 확인한다. 현재 브랜치가 `main` 또는 `develop`이면 PR을 생성하지 말고 작업 브랜치에서 실행해야 한다고 보고한다. +2. base branch는 항상 `develop`으로 고정한다. `main` branch를 base로 하는 PR은 절대 생성하지 않는다. `main` merge와 production 배포는 항상 사용자가 직접 담당한다. +3. `git status`로 미커밋 변경사항을 확인한다. 미커밋 변경사항이 있으면 PR을 생성하지 말고 먼저 커밋 또는 정리가 필요하다고 보고한다. +4. 현재 브랜치가 원격에 push되어 있는지 확인한다. 원격 브랜치가 없거나 미push 커밋이 있으면 PR을 생성할 수 없으므로 사용자에게 push 필요 여부를 확인한다. +5. `develop..현재브랜치`의 `git log`와 `git diff`를 확인해 변경 범위를 파악한다. +6. GitHub에서 `AI-SIP/OnO_BACKEND`의 open issue를 먼저 조회해 현재 변경사항과 관련된 이슈가 있는지 확인한다. + - `mcp__github__.list_issues`를 사용한다. + - 브랜치명, 커밋 메시지, 변경 파일, 작업 키워드와 이슈 제목/본문을 비교한다. + - 실제로 이번 PR에서 해결되는 이슈가 있으면 PR 본문 `연관 이슈` 항목에 `close #이슈번호`를 포함한다. + - 단순 참고 이슈는 자동 종료되지 않도록 `close` 없이 참고 문장으로만 적는다. + - 관련 이슈가 없으면 `연관 이슈` 항목에 `없음`이라고 명시한다. +7. `.github/PULL_REQUEST_TEMPLATE.md`를 반드시 읽고 같은 구조로 PR 본문을 작성한다. +8. 수정된 파일, API/DB/인증/권한/알림/배포 영향 범위를 정리한다. +9. 테스트와 검증은 실행 여부, 명령, 결과를 구분해 적는다. +10. PR 제목은 브랜치명과 커밋 내역을 종합해 `[Feat]`, `[Fix]`, `[Refactor]`, `[Chore]`, `[Docs]`, `[Test]` 중 가장 적절한 타입으로 작성한다. +11. Labels는 변경 성격에 맞게 `feat`, `fix`, `refactor`, `chore`, `docs`, `test`, `bug` 중 적절한 후보를 고르되, `mcp__github__.get_label`로 존재 여부를 확인한 label만 적용한다. +12. `mcp__github__.create_pull_request`로 PR을 생성한다. + - `owner`: `AI-SIP` + - `repo`: `OnO_BACKEND` + - `base`: `develop` + - `head`: 현재 브랜치명 + - `title`: 작성한 PR 제목 + - `body`: 작성한 PR 본문 + - Reviewers는 지정하지 않는다. +13. PR 생성 후 생성된 PR 번호에 `mcp__github__.issue_write`를 `method=update`로 호출해 Assignees와 Labels를 설정한다. + - `owner`: `AI-SIP` + - `repo`: `OnO_BACKEND` + - `issue_number`: 생성된 PR 번호 + - `assignees`: `["KiSeungMin"]` + - `labels`: 존재 확인된 label 배열 +14. milestone, project, issue fields, type 등 나머지 항목은 지정하지 않는다. ## Output -- PR 제목 -- PR 본문 markdown -- 포함한 커밋/미커밋 변경 범위 +- 생성된 PR 번호와 URL +- base/head branch +- Assignee: `KiSeungMin` +- 적용한 label +- 연결한 issue와 `close #이슈번호` 포함 여부 +- `main`을 target으로 사용하지 않았다는 확인 - 사용자 영향 - 검증 결과 - 배포 리스크와 rollback/대응 방법 From 2621f23b60b07c23b7d44faf8b64f3dda35d959a Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sat, 6 Jun 2026 23:17:57 +0900 Subject: [PATCH 14/67] =?UTF-8?q?[Feat]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?-=20=EB=B0=A9=20=EB=A9=A4=EB=B2=84=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=84=B8=EC=85=98=20=EB=A6=AC=ED=8F=AC=ED=8A=B8=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration/V6__add_study_room_schema.sql | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 src/main/resources/db/migration/V6__add_study_room_schema.sql diff --git a/src/main/resources/db/migration/V6__add_study_room_schema.sql b/src/main/resources/db/migration/V6__add_study_room_schema.sql new file mode 100644 index 00000000..ed19705a --- /dev/null +++ b/src/main/resources/db/migration/V6__add_study_room_schema.sql @@ -0,0 +1,173 @@ +CREATE TABLE study_room ( + id BIGINT NOT NULL AUTO_INCREMENT, + version BIGINT, + name VARCHAR(20) NOT NULL, + host_user_id BIGINT NOT NULL, + thumbnail_url VARCHAR(255), + created_at DATETIME(6), + updated_at DATETIME(6), + deleted_at DATETIME(6), + PRIMARY KEY (id) +); +CREATE INDEX idx_study_room_host_user ON study_room (host_user_id); + +CREATE TABLE study_room_member ( + id BIGINT NOT NULL AUTO_INCREMENT, + room_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + role VARCHAR(20) NOT NULL, + weekly_goal INT, + created_at DATETIME(6), + updated_at DATETIME(6), + deleted_at DATETIME(6), + PRIMARY KEY (id), + CONSTRAINT uk_study_room_member_room_user UNIQUE (room_id, user_id), + CONSTRAINT fk_study_room_member_room FOREIGN KEY (room_id) REFERENCES study_room (id) ON DELETE CASCADE, + CONSTRAINT fk_study_room_member_user FOREIGN KEY (user_id) REFERENCES user (id) +); +CREATE INDEX idx_study_room_member_user ON study_room_member (user_id); +CREATE INDEX idx_study_room_member_room ON study_room_member (room_id); + +CREATE TABLE study_room_invite_code ( + id BIGINT NOT NULL AUTO_INCREMENT, + room_id BIGINT NOT NULL, + code VARCHAR(6) NOT NULL, + expired_at DATETIME(6) NOT NULL, + created_at DATETIME(6), + updated_at DATETIME(6), + deleted_at DATETIME(6), + PRIMARY KEY (id), + CONSTRAINT uk_study_room_invite_code UNIQUE (code), + CONSTRAINT fk_study_room_invite_room FOREIGN KEY (room_id) REFERENCES study_room (id) ON DELETE CASCADE +); +CREATE INDEX idx_study_room_invite_room ON study_room_invite_code (room_id); + +CREATE TABLE study_room_feed ( + id BIGINT NOT NULL AUTO_INCREMENT, + room_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + event_type VARCHAR(40) NOT NULL, + metadata_json TEXT, + created_at DATETIME(6), + updated_at DATETIME(6), + deleted_at DATETIME(6), + PRIMARY KEY (id), + CONSTRAINT fk_study_room_feed_room FOREIGN KEY (room_id) REFERENCES study_room (id) ON DELETE CASCADE, + CONSTRAINT fk_study_room_feed_user FOREIGN KEY (user_id) REFERENCES user (id) +); +CREATE INDEX idx_study_room_feed_room_created ON study_room_feed (room_id, created_at); +CREATE INDEX idx_study_room_feed_room_user_type_created ON study_room_feed (room_id, user_id, event_type, created_at); + +CREATE TABLE study_room_feed_reaction ( + id BIGINT NOT NULL AUTO_INCREMENT, + feed_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + emoji VARCHAR(16) NOT NULL, + created_at DATETIME(6), + updated_at DATETIME(6), + deleted_at DATETIME(6), + PRIMARY KEY (id), + CONSTRAINT uk_study_room_feed_reaction UNIQUE (feed_id, user_id, emoji), + CONSTRAINT fk_study_room_feed_reaction_feed FOREIGN KEY (feed_id) REFERENCES study_room_feed (id) ON DELETE CASCADE, + CONSTRAINT fk_study_room_feed_reaction_user FOREIGN KEY (user_id) REFERENCES user (id) +); + +CREATE TABLE study_room_challenge ( + id BIGINT NOT NULL AUTO_INCREMENT, + room_id BIGINT NOT NULL, + title VARCHAR(40) NOT NULL, + type VARCHAR(20) NOT NULL, + metric VARCHAR(40) NOT NULL, + target_value INT NOT NULL, + start_at DATETIME(6) NOT NULL, + end_at DATETIME(6) NOT NULL, + status VARCHAR(20) NOT NULL, + created_at DATETIME(6), + updated_at DATETIME(6), + deleted_at DATETIME(6), + PRIMARY KEY (id), + CONSTRAINT fk_study_room_challenge_room FOREIGN KEY (room_id) REFERENCES study_room (id) ON DELETE CASCADE +); +CREATE INDEX idx_study_room_challenge_room_status_end ON study_room_challenge (room_id, status, end_at); + +CREATE TABLE study_session ( + id BIGINT NOT NULL AUTO_INCREMENT, + room_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + started_at DATETIME(6) NOT NULL, + ended_at DATETIME(6), + duration_minutes INT, + created_at DATETIME(6), + updated_at DATETIME(6), + deleted_at DATETIME(6), + PRIMARY KEY (id), + CONSTRAINT fk_study_session_room FOREIGN KEY (room_id) REFERENCES study_room (id) ON DELETE CASCADE, + CONSTRAINT fk_study_session_user FOREIGN KEY (user_id) REFERENCES user (id) +); +CREATE INDEX idx_study_session_room_ended ON study_session (room_id, ended_at); +CREATE INDEX idx_study_session_user_ended ON study_session (user_id, ended_at); + +CREATE TABLE study_room_shared_problem ( + id BIGINT NOT NULL AUTO_INCREMENT, + room_id BIGINT NOT NULL, + shared_by_user_id BIGINT NOT NULL, + problem_id BIGINT NOT NULL, + comment VARCHAR(100), + created_at DATETIME(6), + updated_at DATETIME(6), + deleted_at DATETIME(6), + PRIMARY KEY (id), + CONSTRAINT fk_study_room_shared_problem_room FOREIGN KEY (room_id) REFERENCES study_room (id) ON DELETE CASCADE, + CONSTRAINT fk_study_room_shared_problem_user FOREIGN KEY (shared_by_user_id) REFERENCES user (id), + CONSTRAINT fk_study_room_shared_problem_problem FOREIGN KEY (problem_id) REFERENCES problem (id) +); +CREATE INDEX idx_study_room_shared_problem_room_created ON study_room_shared_problem (room_id, created_at); +CREATE INDEX idx_study_room_shared_problem_problem ON study_room_shared_problem (problem_id); + +CREATE TABLE study_room_shared_problem_reaction ( + id BIGINT NOT NULL AUTO_INCREMENT, + shared_problem_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + emoji VARCHAR(16) NOT NULL, + created_at DATETIME(6), + updated_at DATETIME(6), + deleted_at DATETIME(6), + PRIMARY KEY (id), + CONSTRAINT uk_study_room_shared_problem_reaction UNIQUE (shared_problem_id, user_id, emoji), + CONSTRAINT fk_study_room_shared_problem_reaction_shared FOREIGN KEY (shared_problem_id) REFERENCES study_room_shared_problem (id) ON DELETE CASCADE, + CONSTRAINT fk_study_room_shared_problem_reaction_user FOREIGN KEY (user_id) REFERENCES user (id) +); + +CREATE TABLE study_room_weekly_report ( + id BIGINT NOT NULL AUTO_INCREMENT, + room_id BIGINT NOT NULL, + week_start DATE NOT NULL, + week_end DATE NOT NULL, + top_member_name VARCHAR(255), + top_member_problem_count INT NOT NULL, + longest_streak_name VARCHAR(255), + longest_streak_days INT NOT NULL, + total_problems INT NOT NULL, + challenges_completed INT NOT NULL, + cheer_message VARCHAR(255) NOT NULL, + created_at DATETIME(6), + updated_at DATETIME(6), + deleted_at DATETIME(6), + PRIMARY KEY (id), + CONSTRAINT uk_study_room_weekly_report UNIQUE (room_id, week_start), + CONSTRAINT fk_study_room_weekly_report_room FOREIGN KEY (room_id) REFERENCES study_room (id) ON DELETE CASCADE +); + +CREATE TABLE study_room_weekly_report_read ( + id BIGINT NOT NULL AUTO_INCREMENT, + report_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + read_at DATETIME(6) NOT NULL, + created_at DATETIME(6), + updated_at DATETIME(6), + deleted_at DATETIME(6), + PRIMARY KEY (id), + CONSTRAINT uk_study_room_weekly_report_read UNIQUE (report_id, user_id), + CONSTRAINT fk_study_room_weekly_report_read_report FOREIGN KEY (report_id) REFERENCES study_room_weekly_report (id) ON DELETE CASCADE, + CONSTRAINT fk_study_room_weekly_report_read_user FOREIGN KEY (user_id) REFERENCES user (id) +); From aee9b107bdfc8506bb99e7d4e69f66a34749665a Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sat, 6 Jun 2026 23:18:08 +0900 Subject: [PATCH 15/67] =?UTF-8?q?[Feat]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EB=A6=AC=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/studyroom/entity/StudyRoom.java | 56 +++++++++++++++ .../studyroom/entity/StudyRoomChallenge.java | 69 ++++++++++++++++++ .../entity/StudyRoomChallengeMetric.java | 7 ++ .../entity/StudyRoomChallengeStatus.java | 8 +++ .../entity/StudyRoomChallengeType.java | 7 ++ .../studyroom/entity/StudyRoomFeed.java | 50 +++++++++++++ .../entity/StudyRoomFeedEventType.java | 11 +++ .../entity/StudyRoomFeedReaction.java | 39 +++++++++++ .../studyroom/entity/StudyRoomInviteCode.java | 44 ++++++++++++ .../studyroom/entity/StudyRoomMember.java | 58 +++++++++++++++ .../studyroom/entity/StudyRoomMemberRole.java | 6 ++ .../entity/StudyRoomSharedProblem.java | 47 +++++++++++++ .../StudyRoomSharedProblemReaction.java | 39 +++++++++++ .../entity/StudyRoomWeeklyReport.java | 70 +++++++++++++++++++ .../entity/StudyRoomWeeklyReportRead.java | 41 +++++++++++ .../studyroom/entity/StudySession.java | 55 +++++++++++++++ .../exception/StudyRoomErrorCase.java | 31 ++++++++ .../StudyRoomChallengeRepository.java | 41 +++++++++++ .../StudyRoomFeedReactionRepository.java | 20 ++++++ .../repository/StudyRoomFeedRepository.java | 31 ++++++++ .../StudyRoomInviteCodeRepository.java | 23 ++++++ .../repository/StudyRoomMemberRepository.java | 40 +++++++++++ .../repository/StudyRoomRepository.java | 22 ++++++ ...dyRoomSharedProblemReactionRepository.java | 20 ++++++ .../StudyRoomSharedProblemRepository.java | 26 +++++++ .../StudyRoomWeeklyReportReadRepository.java | 24 +++++++ .../StudyRoomWeeklyReportRepository.java | 18 +++++ .../repository/StudySessionRepository.java | 26 +++++++ 28 files changed, 929 insertions(+) create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoom.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallenge.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengeMetric.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengeStatus.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengeType.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeed.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeedEventType.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeedReaction.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomInviteCode.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomMember.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomMemberRole.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblem.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblemReaction.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomWeeklyReport.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomWeeklyReportRead.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudySession.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/exception/StudyRoomErrorCase.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomChallengeRepository.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomFeedReactionRepository.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomFeedRepository.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomInviteCodeRepository.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomMemberRepository.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomRepository.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemReactionRepository.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemRepository.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomWeeklyReportReadRepository.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomWeeklyReportRepository.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/repository/StudySessionRepository.java diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoom.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoom.java new file mode 100644 index 00000000..0418e43d --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoom.java @@ -0,0 +1,56 @@ +package com.aisip.OnO.backend.studyroom.entity; + +import com.aisip.OnO.backend.common.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Entity +@Builder(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "study_room", indexes = { + @Index(name = "idx_study_room_host_user", columnList = "host_user_id") +}) +public class StudyRoom extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Version + private Long version; + + @Column(nullable = false, length = 20) + private String name; + + @Column(name = "host_user_id", nullable = false) + private Long hostUserId; + + @Column(name = "thumbnail_url") + private String thumbnailUrl; + + @OneToMany(mappedBy = "room", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default + private List members = new ArrayList<>(); + + public static StudyRoom create(String name, Long hostUserId) { + return StudyRoom.builder() + .name(name) + .hostUserId(hostUserId) + .members(new ArrayList<>()) + .build(); + } + + public void addMember(StudyRoomMember member) { + members.add(member); + member.updateRoom(this); + } + + public void updateHostUserId(Long hostUserId) { + this.hostUserId = hostUserId; + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallenge.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallenge.java new file mode 100644 index 00000000..ff610170 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallenge.java @@ -0,0 +1,69 @@ +package com.aisip.OnO.backend.studyroom.entity; + +import com.aisip.OnO.backend.common.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Entity +@Builder(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "study_room_challenge", indexes = { + @Index(name = "idx_study_room_challenge_room_status_end", columnList = "room_id, status, end_at") +}) +public class StudyRoomChallenge extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + private StudyRoom room; + + @Column(nullable = false, length = 40) + private String title; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20) + private StudyRoomChallengeType type; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 40) + private StudyRoomChallengeMetric metric; + + @Column(name = "target_value", nullable = false) + private Integer targetValue; + + @Column(name = "start_at", nullable = false) + private LocalDateTime startAt; + + @Column(name = "end_at", nullable = false) + private LocalDateTime endAt; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20) + private StudyRoomChallengeStatus status; + + public static StudyRoomChallenge create(StudyRoom room, String title, StudyRoomChallengeType type, + StudyRoomChallengeMetric metric, Integer targetValue, + LocalDateTime startAt, LocalDateTime endAt) { + return StudyRoomChallenge.builder() + .room(room) + .title(title) + .type(type) + .metric(metric) + .targetValue(targetValue) + .startAt(startAt) + .endAt(endAt) + .status(StudyRoomChallengeStatus.IN_PROGRESS) + .build(); + } + + public void updateStatus(StudyRoomChallengeStatus status) { + this.status = status; + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengeMetric.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengeMetric.java new file mode 100644 index 00000000..915db165 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengeMetric.java @@ -0,0 +1,7 @@ +package com.aisip.OnO.backend.studyroom.entity; + +public enum StudyRoomChallengeMetric { + WEEKLY_PROBLEM_COUNT, + WEEKLY_PRACTICE_COUNT, + STREAK +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengeStatus.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengeStatus.java new file mode 100644 index 00000000..d4bc9bac --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengeStatus.java @@ -0,0 +1,8 @@ +package com.aisip.OnO.backend.studyroom.entity; + +public enum StudyRoomChallengeStatus { + IN_PROGRESS, + COMPLETED, + FAILED, + EXPIRED +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengeType.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengeType.java new file mode 100644 index 00000000..fa8a5001 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengeType.java @@ -0,0 +1,7 @@ +package com.aisip.OnO.backend.studyroom.entity; + +public enum StudyRoomChallengeType { + INDIVIDUAL, + GROUP, + STREAK +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeed.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeed.java new file mode 100644 index 00000000..2660e453 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeed.java @@ -0,0 +1,50 @@ +package com.aisip.OnO.backend.studyroom.entity; + +import com.aisip.OnO.backend.common.entity.BaseEntity; +import com.aisip.OnO.backend.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +@Getter +@Entity +@Builder(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "study_room_feed", indexes = { + @Index(name = "idx_study_room_feed_room_created", columnList = "room_id, created_at"), + @Index(name = "idx_study_room_feed_room_user_type_created", columnList = "room_id, user_id, event_type, created_at") +}) +public class StudyRoomFeed extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + private StudyRoom room; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Enumerated(EnumType.STRING) + @Column(name = "event_type", nullable = false, length = 40) + private StudyRoomFeedEventType eventType; + + @Column(name = "metadata_json", columnDefinition = "TEXT") + private String metadataJson; + + public static StudyRoomFeed create(StudyRoom room, User user, StudyRoomFeedEventType eventType, String metadataJson) { + return StudyRoomFeed.builder() + .room(room) + .user(user) + .eventType(eventType) + .metadataJson(metadataJson) + .build(); + } + + public void updateMetadataJson(String metadataJson) { + this.metadataJson = metadataJson; + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeedEventType.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeedEventType.java new file mode 100644 index 00000000..4b1de01d --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeedEventType.java @@ -0,0 +1,11 @@ +package com.aisip.OnO.backend.studyroom.entity; + +public enum StudyRoomFeedEventType { + PROBLEM_REGISTERED, + PRACTICE_COMPLETED, + STREAK_MILESTONE, + LEVEL_UP, + CHALLENGE_CLEARED, + SESSION_STARTED, + PROBLEM_SHARED +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeedReaction.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeedReaction.java new file mode 100644 index 00000000..6ac32267 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeedReaction.java @@ -0,0 +1,39 @@ +package com.aisip.OnO.backend.studyroom.entity; + +import com.aisip.OnO.backend.common.entity.BaseEntity; +import com.aisip.OnO.backend.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +@Getter +@Entity +@Builder(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "study_room_feed_reaction", + uniqueConstraints = @UniqueConstraint(name = "uk_study_room_feed_reaction", columnNames = {"feed_id", "user_id", "emoji"})) +public class StudyRoomFeedReaction extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "feed_id", nullable = false) + private StudyRoomFeed feed; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(nullable = false, length = 16) + private String emoji; + + public static StudyRoomFeedReaction create(StudyRoomFeed feed, User user, String emoji) { + return StudyRoomFeedReaction.builder() + .feed(feed) + .user(user) + .emoji(emoji) + .build(); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomInviteCode.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomInviteCode.java new file mode 100644 index 00000000..146e595a --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomInviteCode.java @@ -0,0 +1,44 @@ +package com.aisip.OnO.backend.studyroom.entity; + +import com.aisip.OnO.backend.common.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Entity +@Builder(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "study_room_invite_code", + uniqueConstraints = @UniqueConstraint(name = "uk_study_room_invite_code", columnNames = "code"), + indexes = @Index(name = "idx_study_room_invite_room", columnList = "room_id")) +public class StudyRoomInviteCode extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + private StudyRoom room; + + @Column(nullable = false, length = 6) + private String code; + + @Column(name = "expired_at", nullable = false) + private LocalDateTime expiredAt; + + public static StudyRoomInviteCode create(StudyRoom room, String code, LocalDateTime expiredAt) { + return StudyRoomInviteCode.builder() + .room(room) + .code(code) + .expiredAt(expiredAt) + .build(); + } + + public boolean isExpired(LocalDateTime now) { + return !expiredAt.isAfter(now); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomMember.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomMember.java new file mode 100644 index 00000000..fc4ed024 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomMember.java @@ -0,0 +1,58 @@ +package com.aisip.OnO.backend.studyroom.entity; + +import com.aisip.OnO.backend.common.entity.BaseEntity; +import com.aisip.OnO.backend.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +@Getter +@Entity +@Builder(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "study_room_member", + uniqueConstraints = @UniqueConstraint(name = "uk_study_room_member_room_user", columnNames = {"room_id", "user_id"}), + indexes = { + @Index(name = "idx_study_room_member_user", columnList = "user_id"), + @Index(name = "idx_study_room_member_room", columnList = "room_id") + }) +public class StudyRoomMember extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + private StudyRoom room; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20) + private StudyRoomMemberRole role; + + @Column(name = "weekly_goal") + private Integer weeklyGoal; + + public static StudyRoomMember create(User user, StudyRoomMemberRole role) { + return StudyRoomMember.builder() + .user(user) + .role(role) + .build(); + } + + public void updateRoom(StudyRoom room) { + this.room = room; + } + + public void updateWeeklyGoal(Integer weeklyGoal) { + this.weeklyGoal = weeklyGoal; + } + + public void promoteToHost() { + this.role = StudyRoomMemberRole.HOST; + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomMemberRole.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomMemberRole.java new file mode 100644 index 00000000..29d8aff9 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomMemberRole.java @@ -0,0 +1,6 @@ +package com.aisip.OnO.backend.studyroom.entity; + +public enum StudyRoomMemberRole { + HOST, + MEMBER +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblem.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblem.java new file mode 100644 index 00000000..b3f298a2 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblem.java @@ -0,0 +1,47 @@ +package com.aisip.OnO.backend.studyroom.entity; + +import com.aisip.OnO.backend.common.entity.BaseEntity; +import com.aisip.OnO.backend.problem.entity.Problem; +import com.aisip.OnO.backend.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +@Getter +@Entity +@Builder(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "study_room_shared_problem", indexes = { + @Index(name = "idx_study_room_shared_problem_room_created", columnList = "room_id, created_at"), + @Index(name = "idx_study_room_shared_problem_problem", columnList = "problem_id") +}) +public class StudyRoomSharedProblem extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + private StudyRoom room; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "shared_by_user_id", nullable = false) + private User sharedByUser; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "problem_id", nullable = false) + private Problem problem; + + @Column(length = 100) + private String comment; + + public static StudyRoomSharedProblem create(StudyRoom room, User sharedByUser, Problem problem, String comment) { + return StudyRoomSharedProblem.builder() + .room(room) + .sharedByUser(sharedByUser) + .problem(problem) + .comment(comment) + .build(); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblemReaction.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblemReaction.java new file mode 100644 index 00000000..b6f08fab --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblemReaction.java @@ -0,0 +1,39 @@ +package com.aisip.OnO.backend.studyroom.entity; + +import com.aisip.OnO.backend.common.entity.BaseEntity; +import com.aisip.OnO.backend.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +@Getter +@Entity +@Builder(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "study_room_shared_problem_reaction", + uniqueConstraints = @UniqueConstraint(name = "uk_study_room_shared_problem_reaction", columnNames = {"shared_problem_id", "user_id", "emoji"})) +public class StudyRoomSharedProblemReaction extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "shared_problem_id", nullable = false) + private StudyRoomSharedProblem sharedProblem; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(nullable = false, length = 16) + private String emoji; + + public static StudyRoomSharedProblemReaction create(StudyRoomSharedProblem sharedProblem, User user, String emoji) { + return StudyRoomSharedProblemReaction.builder() + .sharedProblem(sharedProblem) + .user(user) + .emoji(emoji) + .build(); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomWeeklyReport.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomWeeklyReport.java new file mode 100644 index 00000000..93949cce --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomWeeklyReport.java @@ -0,0 +1,70 @@ +package com.aisip.OnO.backend.studyroom.entity; + +import com.aisip.OnO.backend.common.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDate; + +@Getter +@Entity +@Builder(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "study_room_weekly_report", + uniqueConstraints = @UniqueConstraint(name = "uk_study_room_weekly_report", columnNames = {"room_id", "week_start"})) +public class StudyRoomWeeklyReport extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + private StudyRoom room; + + @Column(name = "week_start", nullable = false) + private LocalDate weekStart; + + @Column(name = "week_end", nullable = false) + private LocalDate weekEnd; + + @Column(name = "top_member_name") + private String topMemberName; + + @Column(name = "top_member_problem_count", nullable = false) + private Integer topMemberProblemCount; + + @Column(name = "longest_streak_name") + private String longestStreakName; + + @Column(name = "longest_streak_days", nullable = false) + private Integer longestStreakDays; + + @Column(name = "total_problems", nullable = false) + private Integer totalProblems; + + @Column(name = "challenges_completed", nullable = false) + private Integer challengesCompleted; + + @Column(name = "cheer_message", nullable = false) + private String cheerMessage; + + public static StudyRoomWeeklyReport create(StudyRoom room, LocalDate weekStart, LocalDate weekEnd, + String topMemberName, int topMemberProblemCount, + String longestStreakName, int longestStreakDays, + int totalProblems, int challengesCompleted, String cheerMessage) { + return StudyRoomWeeklyReport.builder() + .room(room) + .weekStart(weekStart) + .weekEnd(weekEnd) + .topMemberName(topMemberName) + .topMemberProblemCount(topMemberProblemCount) + .longestStreakName(longestStreakName) + .longestStreakDays(longestStreakDays) + .totalProblems(totalProblems) + .challengesCompleted(challengesCompleted) + .cheerMessage(cheerMessage) + .build(); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomWeeklyReportRead.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomWeeklyReportRead.java new file mode 100644 index 00000000..72303507 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomWeeklyReportRead.java @@ -0,0 +1,41 @@ +package com.aisip.OnO.backend.studyroom.entity; + +import com.aisip.OnO.backend.common.entity.BaseEntity; +import com.aisip.OnO.backend.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Entity +@Builder(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "study_room_weekly_report_read", + uniqueConstraints = @UniqueConstraint(name = "uk_study_room_weekly_report_read", columnNames = {"report_id", "user_id"})) +public class StudyRoomWeeklyReportRead extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "report_id", nullable = false) + private StudyRoomWeeklyReport report; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "read_at", nullable = false) + private LocalDateTime readAt; + + public static StudyRoomWeeklyReportRead create(StudyRoomWeeklyReport report, User user, LocalDateTime readAt) { + return StudyRoomWeeklyReportRead.builder() + .report(report) + .user(user) + .readAt(readAt) + .build(); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudySession.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudySession.java new file mode 100644 index 00000000..79ad1e97 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudySession.java @@ -0,0 +1,55 @@ +package com.aisip.OnO.backend.studyroom.entity; + +import com.aisip.OnO.backend.common.entity.BaseEntity; +import com.aisip.OnO.backend.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +import java.time.Duration; +import java.time.LocalDateTime; + +@Getter +@Entity +@Builder(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "study_session", indexes = { + @Index(name = "idx_study_session_room_ended", columnList = "room_id, ended_at"), + @Index(name = "idx_study_session_user_ended", columnList = "user_id, ended_at") +}) +public class StudySession extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + private StudyRoom room; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "started_at", nullable = false) + private LocalDateTime startedAt; + + @Column(name = "ended_at") + private LocalDateTime endedAt; + + @Column(name = "duration_minutes") + private Integer durationMinutes; + + public static StudySession start(StudyRoom room, User user, LocalDateTime startedAt) { + return StudySession.builder() + .room(room) + .user(user) + .startedAt(startedAt) + .build(); + } + + public void end(LocalDateTime endedAt) { + this.endedAt = endedAt; + this.durationMinutes = Math.toIntExact(Math.max(0, Duration.between(startedAt, endedAt).toMinutes())); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/exception/StudyRoomErrorCase.java b/src/main/java/com/aisip/OnO/backend/studyroom/exception/StudyRoomErrorCase.java new file mode 100644 index 00000000..0b95aec6 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/exception/StudyRoomErrorCase.java @@ -0,0 +1,31 @@ +package com.aisip.OnO.backend.studyroom.exception; + +import com.aisip.OnO.backend.common.exception.ErrorCase; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum StudyRoomErrorCase implements ErrorCase { + + STUDY_ROOM_NOT_FOUND(404, 10001, "스터디룸을 찾을 수 없습니다."), + STUDY_ROOM_FORBIDDEN(403, 10002, "스터디룸 멤버만 접근할 수 있습니다."), + STUDY_ROOM_HOST_ONLY(403, 10003, "스터디룸 방장만 사용할 수 있습니다."), + STUDY_ROOM_FULL(409, 10004, "스터디룸 최대 인원을 초과했습니다."), + STUDY_ROOM_LIMIT_EXCEEDED(409, 10005, "참여 가능한 스터디룸 수를 초과했습니다."), + INVITE_CODE_INVALID(400, 10006, "초대 코드가 유효하지 않습니다."), + INVITE_CODE_EXPIRED(400, 10007, "초대 코드가 만료되었습니다."), + ALREADY_MEMBER(409, 10008, "이미 참여 중인 스터디룸입니다."), + CHALLENGE_NOT_FOUND(404, 10009, "챌린지를 찾을 수 없습니다."), + CHALLENGE_LIMIT_EXCEEDED(409, 10010, "진행 중인 챌린지 수를 초과했습니다."), + SESSION_ALREADY_ACTIVE(409, 10011, "이미 진행 중인 공부 세션이 있습니다."), + SESSION_NOT_FOUND(404, 10012, "공부 세션을 찾을 수 없습니다."), + SHARED_PROBLEM_NOT_FOUND(404, 10013, "공유 문제를 찾을 수 없습니다."), + REPORT_NOT_FOUND(404, 10014, "주간 리포트를 찾을 수 없습니다."), + INVALID_REACTION_EMOJI(400, 10015, "허용되지 않은 반응입니다."), + INVALID_STUDY_ROOM_REQUEST(400, 10016, "스터디룸 요청 값이 올바르지 않습니다."); + + private final Integer httpStatusCode; + private final Integer errorCode; + private final String message; +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomChallengeRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomChallengeRepository.java new file mode 100644 index 00000000..f7aa4d84 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomChallengeRepository.java @@ -0,0 +1,41 @@ +package com.aisip.OnO.backend.studyroom.repository; + +import com.aisip.OnO.backend.studyroom.entity.StudyRoomChallenge; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomChallengeStatus; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public interface StudyRoomChallengeRepository extends JpaRepository { + + List findAllByRoomIdOrderByEndAtAsc(Long roomId); + + @Query("select c from StudyRoomChallenge c " + + "where c.room.id = :roomId and c.status = :status and c.endAt >= :now " + + "order by c.endAt asc") + List findActiveByRoomId(@Param("roomId") Long roomId, + @Param("status") StudyRoomChallengeStatus status, + @Param("now") LocalDateTime now); + + Optional findByIdAndRoomId(Long id, Long roomId); + + long countByRoomIdAndStatus(Long roomId, StudyRoomChallengeStatus status); + + @Query("select c from StudyRoomChallenge c where c.room.id in :roomIds and c.status = :status") + List findAllByRoomIdsAndStatus(@Param("roomIds") Collection roomIds, + @Param("status") StudyRoomChallengeStatus status); + + @Query("select c.room.id, count(c.id) from StudyRoomChallenge c " + + "where c.room.id in :roomIds and c.status = :status and c.endAt between :start and :end " + + "group by c.room.id") + List countByRoomIdsAndStatusAndEndAtBetween(@Param("roomIds") Collection roomIds, + @Param("status") StudyRoomChallengeStatus status, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomFeedReactionRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomFeedReactionRepository.java new file mode 100644 index 00000000..d7affac2 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomFeedReactionRepository.java @@ -0,0 +1,20 @@ +package com.aisip.OnO.backend.studyroom.repository; + +import com.aisip.OnO.backend.studyroom.entity.StudyRoomFeedReaction; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +public interface StudyRoomFeedReactionRepository extends JpaRepository { + + Optional findByFeedIdAndUserIdAndEmoji(Long feedId, Long userId, String emoji); + + @Query("select r from StudyRoomFeedReaction r where r.feed.id in :feedIds") + List findAllByFeedIds(@Param("feedIds") Collection feedIds); + + List findAllByFeedId(Long feedId); +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomFeedRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomFeedRepository.java new file mode 100644 index 00000000..287fc0fe --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomFeedRepository.java @@ -0,0 +1,31 @@ +package com.aisip.OnO.backend.studyroom.repository; + +import com.aisip.OnO.backend.studyroom.entity.StudyRoomFeed; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomFeedEventType; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public interface StudyRoomFeedRepository extends JpaRepository { + + @Query("select f from StudyRoomFeed f join fetch f.user where f.room.id = :roomId order by f.createdAt desc") + Page findPageWithUserByRoomId(@Param("roomId") Long roomId, Pageable pageable); + + @Query("select f from StudyRoomFeed f join fetch f.user " + + "where f.room.id = :roomId and (:cursor is null or f.id < :cursor) " + + "order by f.id desc") + List findWithUserByRoomIdAndCursor(@Param("roomId") Long roomId, + @Param("cursor") Long cursor, + Pageable pageable); + + Optional findByIdAndRoomId(Long id, Long roomId); + + Optional findTopByRoomIdAndUserIdAndEventTypeAndCreatedAtBetweenOrderByCreatedAtDesc( + Long roomId, Long userId, StudyRoomFeedEventType eventType, LocalDateTime start, LocalDateTime end); +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomInviteCodeRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomInviteCodeRepository.java new file mode 100644 index 00000000..aff71efc --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomInviteCodeRepository.java @@ -0,0 +1,23 @@ +package com.aisip.OnO.backend.studyroom.repository; + +import com.aisip.OnO.backend.studyroom.entity.StudyRoomInviteCode; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.Optional; + +public interface StudyRoomInviteCodeRepository extends JpaRepository { + + Optional findTopByRoomIdOrderByExpiredAtDesc(Long roomId); + + Optional findByCode(String code); + + boolean existsByCode(String code); + + @Modifying + @Query("delete from StudyRoomInviteCode c where c.expiredAt < :threshold") + void deleteExpiredBefore(@Param("threshold") LocalDateTime threshold); +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomMemberRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomMemberRepository.java new file mode 100644 index 00000000..0b180e64 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomMemberRepository.java @@ -0,0 +1,40 @@ +package com.aisip.OnO.backend.studyroom.repository; + +import com.aisip.OnO.backend.studyroom.entity.StudyRoomMember; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomMemberRole; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; +import java.util.Collection; + +public interface StudyRoomMemberRepository extends JpaRepository { + + Optional findByRoomIdAndUserId(Long roomId, Long userId); + + boolean existsByRoomIdAndUserId(Long roomId, Long userId); + + long countByRoomId(Long roomId); + + long countByUserId(Long userId); + + long countByRoomIdAndRole(Long roomId, StudyRoomMemberRole role); + + List findAllByUserIdOrderByCreatedAtDesc(Long userId); + + @Query("select m from StudyRoomMember m join fetch m.user where m.room.id = :roomId order by m.createdAt asc") + List findAllWithUserByRoomId(@Param("roomId") Long roomId); + + @Query("select m from StudyRoomMember m join fetch m.room join fetch m.user where m.room.id in :roomIds order by m.room.id asc, m.createdAt asc") + List findAllWithRoomAndUserByRoomIds(@Param("roomIds") Collection roomIds); + + @Query("select m from StudyRoomMember m join fetch m.room where m.user.id = :userId order by m.createdAt desc") + List findAllWithRoomByUserId(@Param("userId") Long userId); + + @Modifying + @Query("delete from StudyRoomMember m where m.room.id = :roomId and m.user.id = :userId") + void deleteByRoomIdAndUserId(@Param("roomId") Long roomId, @Param("userId") Long userId); +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomRepository.java new file mode 100644 index 00000000..a3074d19 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomRepository.java @@ -0,0 +1,22 @@ +package com.aisip.OnO.backend.studyroom.repository; + +import com.aisip.OnO.backend.studyroom.entity.StudyRoom; +import jakarta.persistence.LockModeType; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface StudyRoomRepository extends JpaRepository { + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select r from StudyRoom r where r.id = :roomId") + Optional findByIdForUpdate(@Param("roomId") Long roomId); + + @Query("select r from StudyRoom r where r.id > :cursor order by r.id asc") + List findBatchAfterId(@Param("cursor") Long cursor, Pageable pageable); +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemReactionRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemReactionRepository.java new file mode 100644 index 00000000..f4ecc4f2 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemReactionRepository.java @@ -0,0 +1,20 @@ +package com.aisip.OnO.backend.studyroom.repository; + +import com.aisip.OnO.backend.studyroom.entity.StudyRoomSharedProblemReaction; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +public interface StudyRoomSharedProblemReactionRepository extends JpaRepository { + + Optional findBySharedProblemIdAndUserIdAndEmoji(Long sharedProblemId, Long userId, String emoji); + + @Query("select r from StudyRoomSharedProblemReaction r where r.sharedProblem.id in :sharedProblemIds") + List findAllBySharedProblemIds(@Param("sharedProblemIds") Collection sharedProblemIds); + + List findAllBySharedProblemId(Long sharedProblemId); +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemRepository.java new file mode 100644 index 00000000..372cd69a --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemRepository.java @@ -0,0 +1,26 @@ +package com.aisip.OnO.backend.studyroom.repository; + +import com.aisip.OnO.backend.studyroom.entity.StudyRoomSharedProblem; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface StudyRoomSharedProblemRepository extends JpaRepository { + + @Query("select sp from StudyRoomSharedProblem sp join fetch sp.sharedByUser join fetch sp.problem where sp.room.id = :roomId order by sp.createdAt desc") + Page findPageByRoomId(@Param("roomId") Long roomId, Pageable pageable); + + @Query("select sp from StudyRoomSharedProblem sp join fetch sp.sharedByUser join fetch sp.problem " + + "where sp.room.id = :roomId and (:cursor is null or sp.id < :cursor) " + + "order by sp.id desc") + List findByRoomIdAndCursor(@Param("roomId") Long roomId, + @Param("cursor") Long cursor, + Pageable pageable); + + Optional findByIdAndRoomId(Long id, Long roomId); +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomWeeklyReportReadRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomWeeklyReportReadRepository.java new file mode 100644 index 00000000..18e57534 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomWeeklyReportReadRepository.java @@ -0,0 +1,24 @@ +package com.aisip.OnO.backend.studyroom.repository; + +import com.aisip.OnO.backend.studyroom.entity.StudyRoomWeeklyReportRead; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +public interface StudyRoomWeeklyReportReadRepository extends JpaRepository { + + boolean existsByReportIdAndUserId(Long reportId, Long userId); + + Optional findByReportIdAndUserId(Long reportId, Long userId); + + @Query("select r from StudyRoomWeeklyReportRead r where r.report.id in :reportIds and r.user.id = :userId") + List findAllByReportIdsAndUserId(@Param("reportIds") Collection reportIds, @Param("userId") Long userId); + + @Query("select count(rp) > 0 from StudyRoomWeeklyReport rp where rp.room.id = :roomId and not exists " + + "(select rr.id from StudyRoomWeeklyReportRead rr where rr.report = rp and rr.user.id = :userId)") + boolean existsUnreadReport(@Param("roomId") Long roomId, @Param("userId") Long userId); +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomWeeklyReportRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomWeeklyReportRepository.java new file mode 100644 index 00000000..7b915fb5 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomWeeklyReportRepository.java @@ -0,0 +1,18 @@ +package com.aisip.OnO.backend.studyroom.repository; + +import com.aisip.OnO.backend.studyroom.entity.StudyRoomWeeklyReport; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +public interface StudyRoomWeeklyReportRepository extends JpaRepository { + + boolean existsByRoomIdAndWeekStart(Long roomId, LocalDate weekStart); + + Optional findByIdAndRoomId(Long id, Long roomId); + + List findAllByRoomIdOrderByWeekStartDesc(Long roomId, Pageable pageable); +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudySessionRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudySessionRepository.java new file mode 100644 index 00000000..1fac3ce0 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudySessionRepository.java @@ -0,0 +1,26 @@ +package com.aisip.OnO.backend.studyroom.repository; + +import com.aisip.OnO.backend.studyroom.entity.StudySession; +import jakarta.persistence.LockModeType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public interface StudySessionRepository extends JpaRepository { + + @Query("select s from StudySession s join fetch s.user where s.room.id = :roomId and s.endedAt is null order by s.startedAt asc") + List findActiveByRoomId(@Param("roomId") Long roomId); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select s from StudySession s where s.user.id = :userId and s.endedAt is null") + List findActiveByUserIdForUpdate(@Param("userId") Long userId); + + Optional findByIdAndRoomIdAndUserId(Long id, Long roomId, Long userId); + + List findAllByEndedAtIsNullAndStartedAtBefore(LocalDateTime threshold); +} From b8f76a060e99c7d52a63385124728c41d7309098 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sat, 6 Jun 2026 23:18:34 +0900 Subject: [PATCH 16/67] =?UTF-8?q?[Feat]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20=EA=B8=B0=EB=B3=B8=20API=20=EC=B6=94=EA=B0=80=20-?= =?UTF-8?q?=20=EB=B0=A9=20=EC=83=9D=EC=84=B1=20=EC=B4=88=EB=8C=80=20?= =?UTF-8?q?=EC=B0=B8=EC=97=AC=20=ED=83=88=ED=87=B4=20=EB=AA=A9=ED=91=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StudyRoomController.java | 75 ++++++++ .../backend/studyroom/dto/StudyRoomDtos.java | 105 ++++++++++ .../backend/studyroom/dto/StudyRoomStats.java | 4 + .../service/StudyRoomAccessService.java | 43 +++++ .../service/StudyRoomInviteCodeIssuer.java | 50 +++++ .../service/StudyRoomInviteService.java | 82 ++++++++ .../studyroom/service/StudyRoomMapper.java | 66 +++++++ .../studyroom/service/StudyRoomService.java | 136 +++++++++++++ .../service/StudyRoomStatsService.java | 182 ++++++++++++++++++ 9 files changed, 743 insertions(+) create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomStats.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomAccessService.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomInviteCodeIssuer.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomInviteService.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomMapper.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomStatsService.java diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java new file mode 100644 index 00000000..4590bd45 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java @@ -0,0 +1,75 @@ +package com.aisip.OnO.backend.studyroom.controller; + +import com.aisip.OnO.backend.common.response.CommonResponse; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.studyroom.service.StudyRoomInviteService; +import com.aisip.OnO.backend.studyroom.service.StudyRoomService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/study-room") +public class StudyRoomController { + + private final StudyRoomService studyRoomService; + private final StudyRoomInviteService inviteService; + + @GetMapping + public CommonResponse> getMyRooms() { + return CommonResponse.success(studyRoomService.getMyRooms(currentUserId())); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public CommonResponse createRoom(@RequestBody StudyRoomCreateRequest request) { + return CommonResponse.success(studyRoomService.createRoom(request, currentUserId())); + } + + @GetMapping("/{roomId}") + public CommonResponse getRoom(@PathVariable Long roomId) { + return CommonResponse.success(studyRoomService.getRoom(roomId, currentUserId())); + } + + @DeleteMapping("/{roomId}") + public CommonResponse deleteRoom(@PathVariable Long roomId) { + studyRoomService.deleteRoom(roomId, currentUserId()); + return CommonResponse.success("스터디룸 삭제가 완료되었습니다."); + } + + @PostMapping("/{roomId}/invite") + public CommonResponse issueInviteCode(@PathVariable Long roomId) { + return CommonResponse.success(inviteService.issueInviteCode(roomId, currentUserId())); + } + + @PostMapping("/join") + public CommonResponse join(@RequestBody StudyRoomJoinRequest request) { + return CommonResponse.success(inviteService.join(request, currentUserId())); + } + + @DeleteMapping("/{roomId}/leave") + public CommonResponse leaveRoom(@PathVariable Long roomId) { + studyRoomService.leaveRoom(roomId, currentUserId()); + return CommonResponse.success("스터디룸 탈퇴가 완료되었습니다."); + } + + @DeleteMapping("/{roomId}/members/{memberId}") + public CommonResponse kickMember(@PathVariable Long roomId, @PathVariable Long memberId) { + studyRoomService.kickMember(roomId, memberId, currentUserId()); + return CommonResponse.success("스터디룸 멤버 강퇴가 완료되었습니다."); + } + + @PutMapping("/{roomId}/members/me/goal") + public CommonResponse updateGoal(@PathVariable Long roomId, + @RequestBody StudyRoomGoalUpdateRequest request) { + return CommonResponse.success(studyRoomService.updateGoal(roomId, currentUserId(), request)); + } + + private Long currentUserId() { + return (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java new file mode 100644 index 00000000..b727861b --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java @@ -0,0 +1,105 @@ +package com.aisip.OnO.backend.studyroom.dto; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +public final class StudyRoomDtos { + + private StudyRoomDtos() { + } + + public record StudyRoomCreateRequest(String name) { + } + + public record StudyRoomJoinRequest(String code) { + } + + public record StudyRoomGoalUpdateRequest(Integer weeklyGoal) { + } + + public record ReactionToggleRequest(String emoji) { + } + + public record ChallengeCreateRequest(String title, String type, String metric, Integer targetValue, + LocalDateTime startAt, LocalDateTime endAt) { + } + + public record SharedProblemCreateRequest(Long problemId, String comment) { + } + + public record StudyRoomListResponse(Long roomId, String name, Long hostUserId, int memberCount, + String thumbnailUrl, boolean hasUnreadReport) { + } + + public record StudyRoomDetailResponse(Long roomId, String name, Long hostUserId, String thumbnailUrl, + int memberCount, List members) { + } + + public record StudyRoomMemberResponse(Long userId, String name, Long totalStudyLevel, int currentStreak, + int weeklyProblemCount, int weeklyPracticeCount, + Integer weeklyGoal, Integer goalProgress) { + } + + public record InviteCodeResponse(String code, LocalDateTime expiredAt) { + } + + public record GoalUpdateResponse(Integer weeklyGoal, Integer goalProgress) { + } + + public record PageResponse(List content, int page, int size, long totalElements, boolean hasNext) { + } + + public record ReactionResponse(String emoji, long count, boolean reactedByMe) { + } + + public record FeedItemResponse(Long feedId, Long userId, String userName, String eventType, + Map metadata, LocalDateTime createdAt, + List reactions) { + } + + public record FeedReactionToggleResponse(Long feedId, List reactions) { + } + + public record ChallengeResponse(Long challengeId, String title, String type, String metric, Integer targetValue, + LocalDateTime startAt, LocalDateTime endAt, String status, + List memberProgress, + Integer groupCurrent) { + } + + public record ChallengeMemberProgressResponse(Long userId, String name, int current, boolean cleared) { + } + + public record ActiveStudySessionsResponse(List activeSessions) { + } + + public record ActiveStudySessionResponse(Long userId, String name, LocalDateTime startedAt) { + } + + public record StudySessionStartResponse(Long sessionId, LocalDateTime startedAt) { + } + + public record StudySessionEndResponse(Long sessionId, LocalDateTime startedAt, LocalDateTime endedAt, + Integer durationMinutes) { + } + + public record SharedProblemResponse(Long sharedProblemId, Long sharedByUserId, String sharedByName, + Long problemId, String problemImageUrl, String reference, + String comment, LocalDateTime sharedAt, + List reactions) { + } + + public record SharedProblemReactionToggleResponse(Long sharedProblemId, List reactions) { + } + + public record WeeklyReportResponse(Long reportId, LocalDate weekStart, LocalDate weekEnd, + String topMemberName, Integer topMemberProblemCount, + String longestStreakName, Integer longestStreakDays, + Integer totalProblems, Integer challengesCompleted, + String cheerMessage, boolean isRead) { + } + + public record WeeklyReportReadResponse(Long reportId, boolean isRead) { + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomStats.java b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomStats.java new file mode 100644 index 00000000..c8e52f50 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomStats.java @@ -0,0 +1,4 @@ +package com.aisip.OnO.backend.studyroom.dto; + +public record StudyRoomStats(int weeklyProblemCount, int weeklyPracticeCount, int currentStreak) { +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomAccessService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomAccessService.java new file mode 100644 index 00000000..88b91373 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomAccessService.java @@ -0,0 +1,43 @@ +package com.aisip.OnO.backend.studyroom.service; + +import com.aisip.OnO.backend.common.exception.ApplicationException; +import com.aisip.OnO.backend.studyroom.entity.StudyRoom; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomMember; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomMemberRole; +import com.aisip.OnO.backend.studyroom.exception.StudyRoomErrorCase; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomMemberRepository; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class StudyRoomAccessService { + + private final StudyRoomRepository roomRepository; + private final StudyRoomMemberRepository memberRepository; + + public StudyRoom getRoomOrThrow(Long roomId) { + return roomRepository.findById(roomId) + .orElseThrow(() -> new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_NOT_FOUND)); + } + + public StudyRoomMember getMemberOrThrow(Long roomId, Long userId) { + getRoomOrThrow(roomId); + return memberRepository.findByRoomIdAndUserId(roomId, userId) + .orElseThrow(() -> new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_FORBIDDEN)); + } + + public void validateMember(Long roomId, Long userId) { + getMemberOrThrow(roomId, userId); + } + + public void validateHost(Long roomId, Long userId) { + StudyRoomMember member = getMemberOrThrow(roomId, userId); + if (member.getRole() != StudyRoomMemberRole.HOST) { + throw new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_HOST_ONLY); + } + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomInviteCodeIssuer.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomInviteCodeIssuer.java new file mode 100644 index 00000000..1c4e53bb --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomInviteCodeIssuer.java @@ -0,0 +1,50 @@ +package com.aisip.OnO.backend.studyroom.service; + +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.InviteCodeResponse; +import com.aisip.OnO.backend.studyroom.entity.StudyRoom; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomInviteCode; +import com.aisip.OnO.backend.studyroom.exception.StudyRoomErrorCase; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomInviteCodeRepository; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomRepository; +import com.aisip.OnO.backend.common.exception.ApplicationException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.security.SecureRandom; +import java.time.LocalDateTime; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class StudyRoomInviteCodeIssuer { + + private static final int INVITE_CODE_BOUND = 1_000_000; + private static final SecureRandom RANDOM = new SecureRandom(); + + private final StudyRoomRepository roomRepository; + private final StudyRoomInviteCodeRepository inviteCodeRepository; + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public InviteCodeResponse createOrReuse(Long roomId, LocalDateTime now) { + Optional existing = inviteCodeRepository.findTopByRoomIdOrderByExpiredAtDesc(roomId) + .filter(code -> !code.isExpired(now)); + if (existing.isPresent()) { + StudyRoomInviteCode code = existing.get(); + return new InviteCodeResponse(code.getCode(), code.getExpiredAt()); + } + + inviteCodeRepository.deleteExpiredBefore(now.minusDays(7)); + StudyRoom room = roomRepository.findById(roomId) + .orElseThrow(() -> new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_NOT_FOUND)); + String code; + do { + code = "%06d".formatted(RANDOM.nextInt(INVITE_CODE_BOUND)); + } while (inviteCodeRepository.existsByCode(code)); + + StudyRoomInviteCode inviteCode = inviteCodeRepository.saveAndFlush( + StudyRoomInviteCode.create(room, code, now.plusHours(24))); + return new InviteCodeResponse(inviteCode.getCode(), inviteCode.getExpiredAt()); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomInviteService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomInviteService.java new file mode 100644 index 00000000..2d2136f6 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomInviteService.java @@ -0,0 +1,82 @@ +package com.aisip.OnO.backend.studyroom.service; + +import com.aisip.OnO.backend.common.exception.ApplicationException; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.studyroom.entity.StudyRoom; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomInviteCode; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomMember; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomMemberRole; +import com.aisip.OnO.backend.studyroom.exception.StudyRoomErrorCase; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomInviteCodeRepository; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomMemberRepository; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.exception.UserErrorCase; +import com.aisip.OnO.backend.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +public class StudyRoomInviteService { + + private static final int INVITE_CODE_RETRY_COUNT = 5; + + private final StudyRoomAccessService accessService; + private final StudyRoomInviteCodeRepository inviteCodeRepository; + private final StudyRoomMemberRepository memberRepository; + private final UserRepository userRepository; + private final StudyRoomService studyRoomService; + private final StudyRoomInviteCodeIssuer inviteCodeIssuer; + + public InviteCodeResponse issueInviteCode(Long roomId, Long userId) { + accessService.validateMember(roomId, userId); + LocalDateTime now = LocalDateTime.now(); + return inviteCodeRepository.findTopByRoomIdOrderByExpiredAtDesc(roomId) + .filter(code -> !code.isExpired(now)) + .map(code -> new InviteCodeResponse(code.getCode(), code.getExpiredAt())) + .orElseGet(() -> createInviteCode(roomId, now)); + } + + @Transactional + public StudyRoomDetailResponse join(StudyRoomJoinRequest request, Long userId) { + if (request.code() == null || !request.code().matches("\\d{6}")) { + throw new ApplicationException(StudyRoomErrorCase.INVITE_CODE_INVALID); + } + StudyRoomInviteCode inviteCode = inviteCodeRepository.findByCode(request.code()) + .orElseThrow(() -> new ApplicationException(StudyRoomErrorCase.INVITE_CODE_INVALID)); + if (inviteCode.isExpired(LocalDateTime.now())) { + throw new ApplicationException(StudyRoomErrorCase.INVITE_CODE_EXPIRED); + } + StudyRoom room = studyRoomService.lockRoom(inviteCode.getRoom().getId()); + if (memberRepository.existsByRoomIdAndUserId(room.getId(), userId)) { + throw new ApplicationException(StudyRoomErrorCase.ALREADY_MEMBER); + } + if (memberRepository.countByRoomId(room.getId()) >= StudyRoomService.MAX_ROOM_MEMBER_COUNT) { + throw new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_FULL); + } + if (memberRepository.countByUserId(userId) >= StudyRoomService.MAX_USER_ROOM_COUNT) { + throw new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_LIMIT_EXCEEDED); + } + User user = userRepository.findByIdForUpdate(userId) + .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); + room.addMember(StudyRoomMember.create(user, StudyRoomMemberRole.MEMBER)); + return studyRoomService.buildDetail(room); + } + + private InviteCodeResponse createInviteCode(Long roomId, LocalDateTime now) { + for (int attempt = 0; attempt < INVITE_CODE_RETRY_COUNT; attempt++) { + try { + return inviteCodeIssuer.createOrReuse(roomId, now); + } catch (DataIntegrityViolationException ignored) { + if (attempt == INVITE_CODE_RETRY_COUNT - 1) { + throw new ApplicationException(StudyRoomErrorCase.INVALID_STUDY_ROOM_REQUEST); + } + } + } + throw new ApplicationException(StudyRoomErrorCase.INVALID_STUDY_ROOM_REQUEST); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomMapper.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomMapper.java new file mode 100644 index 00000000..d1609923 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomMapper.java @@ -0,0 +1,66 @@ +package com.aisip.OnO.backend.studyroom.service; + +import com.aisip.OnO.backend.mission.entity.UserMissionStatus; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomStats; +import com.aisip.OnO.backend.studyroom.entity.StudyRoom; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomMember; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomWeeklyReportReadRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class StudyRoomMapper { + + private final StudyRoomWeeklyReportReadRepository reportReadRepository; + + public StudyRoomListResponse toListResponse(StudyRoomMember member) { + StudyRoom room = member.getRoom(); + return new StudyRoomListResponse( + room.getId(), + room.getName(), + room.getHostUserId(), + room.getMembers().size(), + room.getThumbnailUrl(), + reportReadRepository.existsUnreadReport(room.getId(), member.getUser().getId()) + ); + } + + public StudyRoomDetailResponse toDetailResponse(StudyRoom room, List members, + Map statsByUserId) { + List memberResponses = members.stream() + .map(member -> toMemberResponse(member, statsByUserId.get(member.getUser().getId()))) + .toList(); + return new StudyRoomDetailResponse( + room.getId(), + room.getName(), + room.getHostUserId(), + room.getThumbnailUrl(), + memberResponses.size(), + memberResponses + ); + } + + public StudyRoomMemberResponse toMemberResponse(StudyRoomMember member, StudyRoomStats stats) { + int weeklyProblemCount = stats == null ? 0 : stats.weeklyProblemCount(); + int weeklyPracticeCount = stats == null ? 0 : stats.weeklyPracticeCount(); + int currentStreak = stats == null ? 0 : stats.currentStreak(); + Integer goalProgress = member.getWeeklyGoal() == null ? null : weeklyProblemCount; + UserMissionStatus missionStatus = member.getUser().getUserMissionStatus(); + Long totalStudyLevel = missionStatus == null ? 1L : missionStatus.getTotalStudyLevel(); + return new StudyRoomMemberResponse( + member.getUser().getId(), + member.getUser().getName(), + totalStudyLevel, + currentStreak, + weeklyProblemCount, + weeklyPracticeCount, + member.getWeeklyGoal(), + goalProgress + ); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java new file mode 100644 index 00000000..ea130286 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java @@ -0,0 +1,136 @@ +package com.aisip.OnO.backend.studyroom.service; + +import com.aisip.OnO.backend.common.exception.ApplicationException; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomStats; +import com.aisip.OnO.backend.studyroom.entity.StudyRoom; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomMember; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomMemberRole; +import com.aisip.OnO.backend.studyroom.exception.StudyRoomErrorCase; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomMemberRepository; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomRepository; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.exception.UserErrorCase; +import com.aisip.OnO.backend.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class StudyRoomService { + + public static final int MAX_ROOM_MEMBER_COUNT = 20; + public static final int MAX_USER_ROOM_COUNT = 10; + + private final StudyRoomRepository roomRepository; + private final StudyRoomMemberRepository memberRepository; + private final UserRepository userRepository; + private final StudyRoomAccessService accessService; + private final StudyRoomStatsService statsService; + private final StudyRoomMapper mapper; + + @Transactional(readOnly = true) + public List getMyRooms(Long userId) { + return memberRepository.findAllWithRoomByUserId(userId).stream() + .map(mapper::toListResponse) + .toList(); + } + + @Transactional(readOnly = true) + public StudyRoomDetailResponse getRoom(Long roomId, Long userId) { + accessService.validateMember(roomId, userId); + StudyRoom room = accessService.getRoomOrThrow(roomId); + return buildDetail(room); + } + + @Transactional + public StudyRoomDetailResponse createRoom(StudyRoomCreateRequest request, Long userId) { + String name = validateName(request.name()); + User user = userRepository.findByIdForUpdate(userId) + .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); + if (memberRepository.countByUserId(userId) >= MAX_USER_ROOM_COUNT) { + throw new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_LIMIT_EXCEEDED); + } + StudyRoom room = StudyRoom.create(name, userId); + room.addMember(StudyRoomMember.create(user, StudyRoomMemberRole.HOST)); + roomRepository.save(room); + return buildDetail(room); + } + + @Transactional + public void deleteRoom(Long roomId, Long userId) { + accessService.validateHost(roomId, userId); + StudyRoom room = lockRoom(roomId); + roomRepository.delete(room); + } + + @Transactional + public StudyRoom lockRoom(Long roomId) { + return roomRepository.findByIdForUpdate(roomId) + .orElseThrow(() -> new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_NOT_FOUND)); + } + + @Transactional + public void leaveRoom(Long roomId, Long userId) { + StudyRoomMember member = accessService.getMemberOrThrow(roomId, userId); + if (member.getRole() != StudyRoomMemberRole.HOST) { + memberRepository.deleteByRoomIdAndUserId(roomId, userId); + return; + } + + StudyRoom room = roomRepository.findByIdForUpdate(roomId) + .orElseThrow(() -> new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_NOT_FOUND)); + List members = memberRepository.findAllWithUserByRoomId(roomId); + StudyRoomMember nextHost = members.stream() + .filter(candidate -> !candidate.getUser().getId().equals(userId)) + .findFirst() + .orElse(null); + if (nextHost == null) { + roomRepository.delete(room); + return; + } + nextHost.promoteToHost(); + room.updateHostUserId(nextHost.getUser().getId()); + memberRepository.deleteByRoomIdAndUserId(roomId, userId); + } + + @Transactional + public void kickMember(Long roomId, Long memberUserId, Long hostUserId) { + accessService.validateHost(roomId, hostUserId); + StudyRoomMember member = accessService.getMemberOrThrow(roomId, memberUserId); + if (member.getRole() == StudyRoomMemberRole.HOST) { + throw new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_HOST_ONLY); + } + memberRepository.deleteByRoomIdAndUserId(roomId, memberUserId); + } + + @Transactional + public GoalUpdateResponse updateGoal(Long roomId, Long userId, StudyRoomGoalUpdateRequest request) { + StudyRoomMember member = accessService.getMemberOrThrow(roomId, userId); + Integer weeklyGoal = request.weeklyGoal(); + if (weeklyGoal != null && weeklyGoal < 0) { + throw new ApplicationException(StudyRoomErrorCase.INVALID_STUDY_ROOM_REQUEST); + } + member.updateWeeklyGoal(weeklyGoal == null || weeklyGoal == 0 ? null : weeklyGoal); + StudyRoomStats stats = statsService.getWeeklyStats(List.of(userId)).get(userId); + return new GoalUpdateResponse(member.getWeeklyGoal(), member.getWeeklyGoal() == null ? null : stats.weeklyProblemCount()); + } + + StudyRoomDetailResponse buildDetail(StudyRoom room) { + List members = memberRepository.findAllWithUserByRoomId(room.getId()); + List userIds = members.stream().map(member -> member.getUser().getId()).toList(); + Map stats = statsService.getWeeklyStats(userIds); + return mapper.toDetailResponse(room, members, stats); + } + + private String validateName(String name) { + if (name == null || name.isBlank() || name.length() > 20) { + throw new ApplicationException(StudyRoomErrorCase.INVALID_STUDY_ROOM_REQUEST); + } + return name.trim(); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomStatsService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomStatsService.java new file mode 100644 index 00000000..6d4ed92c --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomStatsService.java @@ -0,0 +1,182 @@ +package com.aisip.OnO.backend.studyroom.service; + +import com.aisip.OnO.backend.studyroom.dto.StudyRoomStats; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.temporal.TemporalAdjusters; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; +import java.util.Collection; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class StudyRoomStatsService { + + private static final ZoneId KST = ZoneId.of("Asia/Seoul"); + + private final EntityManager entityManager; + + public Map getWeeklyStats(Collection userIds) { + LocalDate today = LocalDate.now(KST); + LocalDate startDate = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); + LocalDateTime start = startDate.atStartOfDay(); + LocalDateTime end = today.atTime(LocalTime.MAX); + return getStats(userIds, start, end, today); + } + + public Map getStats(Collection userIds, LocalDateTime start, LocalDateTime end, LocalDate today) { + List ids = userIds.stream().distinct().toList(); + Map streaks = currentStreaks(ids, today); + return getStats(ids, start, end, streaks); + } + + public Map getStats(Collection userIds, LocalDateTime start, LocalDateTime end, + Map streaks) { + List ids = userIds.stream().distinct().toList(); + Map problems = countProblems(userIds, start, end); + Map practices = countPractices(userIds, start, end); + Map result = new HashMap<>(); + for (Long userId : ids) { + result.put(userId, new StudyRoomStats( + problems.getOrDefault(userId, 0), + practices.getOrDefault(userId, 0), + streaks.getOrDefault(userId, 0) + )); + } + return result; + } + + public Map currentStreaks(Collection userIds, LocalDate today) { + return currentStreaks(userIds, today, null); + } + + public Map currentStreaks(Collection userIds, LocalDate today, LocalDate startDate) { + List ids = userIds.stream().distinct().toList(); + if (ids.isEmpty()) { + return Map.of(); + } + LocalDateTime startAt = (startDate == null ? LocalDate.of(1970, 1, 1) : startDate).atStartOfDay(); + Map> studyDates = new HashMap<>(); + addStudyDates(studyDates, findProblemStudyDates(ids, today, startAt)); + addStudyDates(studyDates, findPracticeStudyDates(ids, today, startAt)); + + Map result = new HashMap<>(); + for (Long userId : ids) { + result.put(userId, currentStreak(studyDates.getOrDefault(userId, new TreeSet<>()), today)); + } + return result; + } + + private Map countProblems(Collection userIds, LocalDateTime start, LocalDateTime end) { + if (userIds.isEmpty()) { + return Map.of(); + } + List rows = entityManager.createQuery(""" + select p.userId, count(p.id) + from Problem p + where p.userId in :userIds and p.createdAt between :start and :end + group by p.userId + """, Object[].class) + .setParameter("userIds", userIds) + .setParameter("start", start) + .setParameter("end", end) + .getResultList(); + return toCountMap(rows); + } + + private Map countPractices(Collection userIds, LocalDateTime start, LocalDateTime end) { + if (userIds.isEmpty()) { + return Map.of(); + } + List rows = entityManager.createQuery(""" + select ps.userId, count(ps.id) + from ProblemSolve ps + where ps.userId in :userIds and ps.practicedAt between :start and :end + group by ps.userId + """, Object[].class) + .setParameter("userIds", userIds) + .setParameter("start", start) + .setParameter("end", end) + .getResultList(); + return toCountMap(rows); + } + + private Map toCountMap(List rows) { + Map result = new HashMap<>(); + for (Object[] row : rows) { + result.put((Long) row[0], Math.toIntExact((Long) row[1])); + } + return result; + } + + private List findProblemStudyDates(List userIds, LocalDate today, LocalDateTime startAt) { + return entityManager.createNativeQuery(""" + select p.user_id, date(p.created_at) + from problem p + where p.user_id in (:userIds) + and p.created_at >= :startAt + and p.created_at < :endExclusive + and p.deleted_at is null + group by p.user_id, date(p.created_at) + """) + .setParameter("userIds", userIds) + .setParameter("startAt", startAt) + .setParameter("endExclusive", today.plusDays(1).atStartOfDay()) + .getResultList(); + } + + private List findPracticeStudyDates(List userIds, LocalDate today, LocalDateTime startAt) { + return entityManager.createNativeQuery(""" + select ps.user_id, date(ps.practiced_at) + from problem_solve ps + where ps.user_id in (:userIds) + and ps.practiced_at >= :startAt + and ps.practiced_at < :endExclusive + and ps.deleted_at is null + group by ps.user_id, date(ps.practiced_at) + """) + .setParameter("userIds", userIds) + .setParameter("startAt", startAt) + .setParameter("endExclusive", today.plusDays(1).atStartOfDay()) + .getResultList(); + } + + private void addStudyDates(Map> studyDates, List rows) { + for (Object[] row : rows) { + Long userId = ((Number) row[0]).longValue(); + LocalDate date = toLocalDate(row[1]); + studyDates.computeIfAbsent(userId, key -> new TreeSet<>()).add(date); + } + } + + private LocalDate toLocalDate(Object value) { + if (value instanceof java.sql.Date date) { + return date.toLocalDate(); + } + if (value instanceof LocalDate date) { + return date; + } + return LocalDate.parse(value.toString()); + } + + private int currentStreak(TreeSet dates, LocalDate today) { + LocalDate cursor = dates.contains(today) ? today : today.minusDays(1); + int streak = 0; + while (dates.contains(cursor)) { + streak++; + cursor = cursor.minusDays(1); + } + return streak; + } +} From 6f696bab4c95c7a389f23f09386c775f258797c8 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sat, 6 Jun 2026 23:19:08 +0900 Subject: [PATCH 17/67] =?UTF-8?q?[Feat]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20=ED=99=9C=EB=8F=99=20API=20=EC=B6=94=EA=B0=80=20-?= =?UTF-8?q?=20=ED=94=BC=EB=93=9C=20=EC=84=B8=EC=85=98=20=EC=B1=8C=EB=A6=B0?= =?UTF-8?q?=EC=A7=80=20=EB=AC=B8=EC=A0=9C=20=EA=B3=B5=EC=9C=A0=20=EB=B0=98?= =?UTF-8?q?=EC=9D=91=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StudyRoomChallengeController.java | 41 +++++ .../controller/StudyRoomFeedController.java | 35 ++++ .../StudyRoomSharedProblemController.java | 49 ++++++ .../controller/StudySessionController.java | 37 ++++ .../event/StudyRoomActivityEvent.java | 8 + .../service/StudyRoomChallengeService.java | 164 ++++++++++++++++++ .../service/StudyRoomFeedService.java | 159 +++++++++++++++++ .../service/StudyRoomReactionService.java | 46 +++++ .../StudyRoomSharedProblemService.java | 131 ++++++++++++++ .../service/StudySessionService.java | 70 ++++++++ 10 files changed, 740 insertions(+) create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomChallengeController.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomFeedController.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomSharedProblemController.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/controller/StudySessionController.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/event/StudyRoomActivityEvent.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedService.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomReactionService.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/service/StudySessionService.java diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomChallengeController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomChallengeController.java new file mode 100644 index 00000000..7d9c4454 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomChallengeController.java @@ -0,0 +1,41 @@ +package com.aisip.OnO.backend.studyroom.controller; + +import com.aisip.OnO.backend.common.response.CommonResponse; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.studyroom.service.StudyRoomChallengeService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/study-room/{roomId}/challenges") +public class StudyRoomChallengeController { + + private final StudyRoomChallengeService challengeService; + + @GetMapping + public CommonResponse> getChallenges(@PathVariable Long roomId) { + return CommonResponse.success(challengeService.getChallenges(roomId, currentUserId())); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public CommonResponse createChallenge(@PathVariable Long roomId, + @RequestBody ChallengeCreateRequest request) { + return CommonResponse.success(challengeService.createChallenge(roomId, currentUserId(), request)); + } + + @DeleteMapping("/{challengeId}") + public CommonResponse deleteChallenge(@PathVariable Long roomId, @PathVariable Long challengeId) { + challengeService.deleteChallenge(roomId, challengeId, currentUserId()); + return CommonResponse.success("챌린지 삭제가 완료되었습니다."); + } + + private Long currentUserId() { + return (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomFeedController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomFeedController.java new file mode 100644 index 00000000..9f886f1d --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomFeedController.java @@ -0,0 +1,35 @@ +package com.aisip.OnO.backend.studyroom.controller; + +import com.aisip.OnO.backend.common.response.CommonResponse; +import com.aisip.OnO.backend.common.response.CursorPageResponse; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.studyroom.service.StudyRoomFeedService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/study-room/{roomId}/feed") +public class StudyRoomFeedController { + + private final StudyRoomFeedService feedService; + + @GetMapping + public CommonResponse> getFeed(@PathVariable Long roomId, + @RequestParam(value = "cursor", required = false) Long cursor, + @RequestParam(defaultValue = "30") int size) { + return CommonResponse.success(feedService.getFeed(roomId, currentUserId(), cursor, size)); + } + + @PostMapping("/{feedId}/reactions") + public CommonResponse toggleReaction(@PathVariable Long roomId, + @PathVariable Long feedId, + @RequestBody ReactionToggleRequest request) { + return CommonResponse.success(feedService.toggleReaction(roomId, feedId, currentUserId(), request)); + } + + private Long currentUserId() { + return (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomSharedProblemController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomSharedProblemController.java new file mode 100644 index 00000000..728da008 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomSharedProblemController.java @@ -0,0 +1,49 @@ +package com.aisip.OnO.backend.studyroom.controller; + +import com.aisip.OnO.backend.common.response.CommonResponse; +import com.aisip.OnO.backend.common.response.CursorPageResponse; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.studyroom.service.StudyRoomSharedProblemService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/study-room/{roomId}/shared-problems") +public class StudyRoomSharedProblemController { + + private final StudyRoomSharedProblemService sharedProblemService; + + @GetMapping + public CommonResponse> getSharedProblems(@PathVariable Long roomId, + @RequestParam(value = "cursor", required = false) Long cursor, + @RequestParam(defaultValue = "20") int size) { + return CommonResponse.success(sharedProblemService.getSharedProblems(roomId, currentUserId(), cursor, size)); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public CommonResponse shareProblem(@PathVariable Long roomId, + @RequestBody SharedProblemCreateRequest request) { + return CommonResponse.success(sharedProblemService.shareProblem(roomId, currentUserId(), request)); + } + + @PostMapping("/{sharedProblemId}/reactions") + public CommonResponse toggleReaction(@PathVariable Long roomId, + @PathVariable Long sharedProblemId, + @RequestBody ReactionToggleRequest request) { + return CommonResponse.success(sharedProblemService.toggleReaction(roomId, sharedProblemId, currentUserId(), request)); + } + + @DeleteMapping("/{sharedProblemId}") + public CommonResponse deleteSharedProblem(@PathVariable Long roomId, @PathVariable Long sharedProblemId) { + sharedProblemService.deleteSharedProblem(roomId, sharedProblemId, currentUserId()); + return CommonResponse.success("공유 문제 삭제가 완료되었습니다."); + } + + private Long currentUserId() { + return (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudySessionController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudySessionController.java new file mode 100644 index 00000000..ff9fd2b1 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudySessionController.java @@ -0,0 +1,37 @@ +package com.aisip.OnO.backend.studyroom.controller; + +import com.aisip.OnO.backend.common.response.CommonResponse; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.studyroom.service.StudySessionService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/study-room/{roomId}/sessions") +public class StudySessionController { + + private final StudySessionService sessionService; + + @GetMapping("/active") + public CommonResponse getActiveSessions(@PathVariable Long roomId) { + return CommonResponse.success(sessionService.getActiveSessions(roomId, currentUserId())); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public CommonResponse start(@PathVariable Long roomId) { + return CommonResponse.success(sessionService.start(roomId, currentUserId())); + } + + @PatchMapping("/{sessionId}/end") + public CommonResponse end(@PathVariable Long roomId, @PathVariable Long sessionId) { + return CommonResponse.success(sessionService.end(roomId, sessionId, currentUserId())); + } + + private Long currentUserId() { + return (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/event/StudyRoomActivityEvent.java b/src/main/java/com/aisip/OnO/backend/studyroom/event/StudyRoomActivityEvent.java new file mode 100644 index 00000000..edd174cc --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/event/StudyRoomActivityEvent.java @@ -0,0 +1,8 @@ +package com.aisip.OnO.backend.studyroom.event; + +import com.aisip.OnO.backend.studyroom.entity.StudyRoomFeedEventType; + +import java.util.Map; + +public record StudyRoomActivityEvent(Long userId, StudyRoomFeedEventType eventType, Map metadata) { +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java new file mode 100644 index 00000000..95d6d592 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java @@ -0,0 +1,164 @@ +package com.aisip.OnO.backend.studyroom.service; + +import com.aisip.OnO.backend.common.exception.ApplicationException; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomStats; +import com.aisip.OnO.backend.studyroom.entity.*; +import com.aisip.OnO.backend.studyroom.exception.StudyRoomErrorCase; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomChallengeRepository; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomMemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class StudyRoomChallengeService { + + private static final int MAX_IN_PROGRESS_CHALLENGE_COUNT = 5; + + private final StudyRoomAccessService accessService; + private final StudyRoomChallengeRepository challengeRepository; + private final StudyRoomMemberRepository memberRepository; + private final StudyRoomStatsService statsService; + + @Transactional(readOnly = true) + public List getChallenges(Long roomId, Long userId) { + accessService.validateMember(roomId, userId); + List members = memberRepository.findAllWithUserByRoomId(roomId); + return challengeRepository.findActiveByRoomId(roomId, StudyRoomChallengeStatus.IN_PROGRESS, LocalDateTime.now()).stream() + .map(challenge -> toResponse(challenge, members, false)) + .toList(); + } + + @Transactional + public ChallengeResponse createChallenge(Long roomId, Long userId, ChallengeCreateRequest request) { + accessService.validateHost(roomId, userId); + validateCreateRequest(request); + refreshRoomChallengeStatuses(roomId); + if (challengeRepository.countByRoomIdAndStatus(roomId, StudyRoomChallengeStatus.IN_PROGRESS) >= MAX_IN_PROGRESS_CHALLENGE_COUNT) { + throw new ApplicationException(StudyRoomErrorCase.CHALLENGE_LIMIT_EXCEEDED); + } + LocalDateTime startAt = request.startAt() == null ? LocalDateTime.now() : request.startAt(); + StudyRoom room = accessService.getRoomOrThrow(roomId); + StudyRoomChallenge challenge = challengeRepository.save(StudyRoomChallenge.create( + room, + request.title().trim(), + parseEnum(request.type(), StudyRoomChallengeType.class), + parseEnum(request.metric(), StudyRoomChallengeMetric.class), + request.targetValue(), + startAt, + request.endAt() + )); + return toResponse(challenge, memberRepository.findAllWithUserByRoomId(roomId), true); + } + + @Transactional + public void deleteChallenge(Long roomId, Long challengeId, Long userId) { + accessService.validateHost(roomId, userId); + StudyRoomChallenge challenge = challengeRepository.findByIdAndRoomId(challengeId, roomId) + .orElseThrow(() -> new ApplicationException(StudyRoomErrorCase.CHALLENGE_NOT_FOUND)); + challengeRepository.delete(challenge); + } + + private void refreshRoomChallengeStatuses(Long roomId) { + List members = memberRepository.findAllWithUserByRoomId(roomId); + challengeRepository.findAllByRoomIdOrderByEndAtAsc(roomId) + .forEach(challenge -> toResponse(challenge, members, true)); + } + + private ChallengeResponse toResponse(StudyRoomChallenge challenge, List members, boolean persistStatus) { + List userIds = members.stream().map(member -> member.getUser().getId()).toList(); + LocalDate streakBaseDate = min(LocalDate.now(), challenge.getEndAt().toLocalDate()); + Map streaks = statsService.currentStreaks(userIds, streakBaseDate, challenge.getStartAt().toLocalDate()); + Map stats = statsService.getStats(userIds, challenge.getStartAt(), challenge.getEndAt(), streaks); + List memberProgress = challenge.getType() == StudyRoomChallengeType.GROUP + ? List.of() + : members.stream() + .map(member -> { + int current = metricValue(challenge.getMetric(), stats.get(member.getUser().getId())); + return new ChallengeMemberProgressResponse(member.getUser().getId(), member.getUser().getName(), + current, current >= challenge.getTargetValue()); + }) + .toList(); + Integer groupCurrent = challenge.getType() == StudyRoomChallengeType.GROUP + ? stats.values().stream().mapToInt(stat -> metricValue(challenge.getMetric(), stat)).sum() + : null; + StudyRoomChallengeStatus status = effectiveStatus(challenge, memberProgress, groupCurrent); + if (persistStatus && challenge.getStatus() != status) { + challenge.updateStatus(status); + } + return new ChallengeResponse( + challenge.getId(), + challenge.getTitle(), + toApiValue(challenge.getType().name()), + toApiValue(challenge.getMetric().name()), + challenge.getTargetValue(), + challenge.getStartAt(), + challenge.getEndAt(), + toApiValue(status.name()), + memberProgress, + groupCurrent + ); + } + + private StudyRoomChallengeStatus effectiveStatus(StudyRoomChallenge challenge, + List memberProgress, + Integer groupCurrent) { + if (challenge.getStatus() != StudyRoomChallengeStatus.IN_PROGRESS) { + return challenge.getStatus(); + } + boolean completed = challenge.getType() == StudyRoomChallengeType.GROUP + ? groupCurrent != null && groupCurrent >= challenge.getTargetValue() + : !memberProgress.isEmpty() && memberProgress.stream().allMatch(ChallengeMemberProgressResponse::cleared); + if (completed) { + return StudyRoomChallengeStatus.COMPLETED; + } + if (challenge.getEndAt().isBefore(LocalDateTime.now())) { + return StudyRoomChallengeStatus.EXPIRED; + } + return StudyRoomChallengeStatus.IN_PROGRESS; + } + + private int metricValue(StudyRoomChallengeMetric metric, StudyRoomStats stats) { + if (stats == null) { + return 0; + } + return switch (metric) { + case WEEKLY_PROBLEM_COUNT -> stats.weeklyProblemCount(); + case WEEKLY_PRACTICE_COUNT -> stats.weeklyPracticeCount(); + case STREAK -> stats.currentStreak(); + }; + } + + private void validateCreateRequest(ChallengeCreateRequest request) { + if (request.title() == null || request.title().isBlank() || request.title().length() > 40 + || request.targetValue() == null || request.targetValue() < 1 + || request.endAt() == null || !request.endAt().isAfter(LocalDateTime.now()) + || request.startAt() != null && !request.startAt().isBefore(request.endAt())) { + throw new ApplicationException(StudyRoomErrorCase.INVALID_STUDY_ROOM_REQUEST); + } + } + + private LocalDate min(LocalDate left, LocalDate right) { + return left.isBefore(right) ? left : right; + } + + private > T parseEnum(String value, Class enumClass) { + try { + return Enum.valueOf(enumClass, value.toUpperCase(Locale.ROOT)); + } catch (Exception e) { + throw new ApplicationException(StudyRoomErrorCase.INVALID_STUDY_ROOM_REQUEST); + } + } + + private String toApiValue(String enumName) { + return enumName.toLowerCase(Locale.ROOT); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedService.java new file mode 100644 index 00000000..518c983f --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedService.java @@ -0,0 +1,159 @@ +package com.aisip.OnO.backend.studyroom.service; + +import com.aisip.OnO.backend.common.exception.ApplicationException; +import com.aisip.OnO.backend.common.response.CursorPageResponse; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.studyroom.entity.*; +import com.aisip.OnO.backend.studyroom.event.StudyRoomActivityEvent; +import com.aisip.OnO.backend.studyroom.exception.StudyRoomErrorCase; +import com.aisip.OnO.backend.studyroom.repository.*; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.exception.UserErrorCase; +import com.aisip.OnO.backend.user.repository.UserRepository; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.util.*; +import java.util.stream.Collectors; + +import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW; +import static org.springframework.transaction.event.TransactionPhase.AFTER_COMMIT; + +@Slf4j +@Service +@RequiredArgsConstructor +public class StudyRoomFeedService { + + private static final ZoneId KST = ZoneId.of("Asia/Seoul"); + private static final Set ALLOWED_FEED_EMOJIS = Set.of("🔥", "👍", "🎉"); + private static final TypeReference> MAP_TYPE = new TypeReference<>() { + }; + + private final StudyRoomAccessService accessService; + private final StudyRoomMemberRepository memberRepository; + private final StudyRoomFeedRepository feedRepository; + private final StudyRoomFeedReactionRepository reactionRepository; + private final UserRepository userRepository; + private final ObjectMapper objectMapper; + private final StudyRoomReactionService reactionService; + + @Transactional(readOnly = true) + public CursorPageResponse getFeed(Long roomId, Long userId, Long cursor, int size) { + accessService.validateMember(roomId, userId); + int safeSize = Math.min(size, 50); + List feeds = feedRepository.findWithUserByRoomIdAndCursor(roomId, cursor, PageRequest.of(0, safeSize + 1)); + boolean hasNext = feeds.size() > safeSize; + List contentFeeds = hasNext ? feeds.subList(0, safeSize) : feeds; + Map> reactions = reactionRepository.findAllByFeedIds( + contentFeeds.stream().map(StudyRoomFeed::getId).toList()) + .stream() + .collect(Collectors.groupingBy(reaction -> reaction.getFeed().getId())); + List content = contentFeeds.stream() + .map(feed -> toFeedItem(feed, reactions.getOrDefault(feed.getId(), List.of()), userId)) + .toList(); + Long nextCursor = hasNext && !contentFeeds.isEmpty() ? contentFeeds.get(contentFeeds.size() - 1).getId() : null; + return new CursorPageResponse<>(content, nextCursor, hasNext, safeSize); + } + + @Transactional + public FeedReactionToggleResponse toggleReaction(Long roomId, Long feedId, Long userId, ReactionToggleRequest request) { + accessService.validateMember(roomId, userId); + if (!ALLOWED_FEED_EMOJIS.contains(request.emoji())) { + throw new ApplicationException(StudyRoomErrorCase.INVALID_REACTION_EMOJI); + } + StudyRoomFeed feed = feedRepository.findByIdAndRoomId(feedId, roomId) + .orElseThrow(() -> new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_NOT_FOUND)); + User user = userRepository.findById(userId) + .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); + reactionRepository.findByFeedIdAndUserIdAndEmoji(feedId, userId, request.emoji()) + .ifPresentOrElse(reactionRepository::delete, + () -> reactionRepository.save(StudyRoomFeedReaction.create(feed, user, request.emoji()))); + List reactions = reactionService.summarizeFeedReactions(reactionRepository.findAllByFeedId(feedId), userId); + return new FeedReactionToggleResponse(feedId, reactions); + } + + @TransactionalEventListener(phase = AFTER_COMMIT) + @Transactional(propagation = REQUIRES_NEW) + public void handleActivity(StudyRoomActivityEvent event) { + try { + createFeedsForUserRooms(event.userId(), event.eventType(), event.metadata()); + } catch (Exception e) { + log.error("Failed to create study room feed. userId: {}, eventType: {}", event.userId(), event.eventType(), e); + } + } + + @Transactional + public void createFeedsForUserRooms(Long userId, StudyRoomFeedEventType eventType, Map metadata) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); + List memberships = memberRepository.findAllWithRoomByUserId(userId); + for (StudyRoomMember membership : memberships) { + createOrAccumulate(membership.getRoom(), user, eventType, metadata == null ? Map.of() : metadata); + } + } + + private void createOrAccumulate(StudyRoom room, User user, StudyRoomFeedEventType eventType, Map metadata) { + if (eventType == StudyRoomFeedEventType.PROBLEM_REGISTERED) { + LocalDate today = LocalDate.now(KST); + LocalDateTime start = today.atStartOfDay(); + LocalDateTime end = today.atTime(LocalTime.MAX); + Optional existing = feedRepository + .findTopByRoomIdAndUserIdAndEventTypeAndCreatedAtBetweenOrderByCreatedAtDesc( + room.getId(), user.getId(), eventType, start, end); + if (existing.isPresent()) { + Map current = readMetadata(existing.get().getMetadataJson()); + int oldCount = ((Number) current.getOrDefault("count", 0)).intValue(); + int newCount = ((Number) metadata.getOrDefault("count", 1)).intValue(); + current.put("count", oldCount + newCount); + existing.get().updateMetadataJson(writeMetadata(current)); + return; + } + } + feedRepository.save(StudyRoomFeed.create(room, user, eventType, writeMetadata(metadata))); + } + + private FeedItemResponse toFeedItem(StudyRoomFeed feed, List reactions, Long userId) { + return new FeedItemResponse( + feed.getId(), + feed.getUser().getId(), + feed.getUser().getName(), + toApiValue(feed.getEventType().name()), + readMetadata(feed.getMetadataJson()), + feed.getCreatedAt(), + reactionService.summarizeFeedReactions(reactions, userId) + ); + } + + private Map readMetadata(String metadataJson) { + if (metadataJson == null || metadataJson.isBlank()) { + return new LinkedHashMap<>(); + } + try { + return objectMapper.readValue(metadataJson, MAP_TYPE); + } catch (Exception e) { + return new LinkedHashMap<>(); + } + } + + private String writeMetadata(Map metadata) { + try { + return objectMapper.writeValueAsString(metadata == null ? Map.of() : metadata); + } catch (Exception e) { + throw new ApplicationException(StudyRoomErrorCase.INVALID_STUDY_ROOM_REQUEST); + } + } + + static String toApiValue(String enumName) { + return enumName.toLowerCase(Locale.ROOT); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomReactionService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomReactionService.java new file mode 100644 index 00000000..397f28f6 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomReactionService.java @@ -0,0 +1,46 @@ +package com.aisip.OnO.backend.studyroom.service; + +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.ReactionResponse; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomFeedReaction; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomSharedProblemReaction; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +@Component +public class StudyRoomReactionService { + + public List summarizeFeedReactions(Collection reactions, Long userId) { + return summarize(reactions, userId, StudyRoomFeedReaction::getEmoji, reaction -> reaction.getUser().getId()); + } + + public List summarizeSharedProblemReactions(Collection reactions, Long userId) { + return summarize(reactions, userId, StudyRoomSharedProblemReaction::getEmoji, reaction -> reaction.getUser().getId()); + } + + private List summarize(Collection reactions, Long userId, + Function emojiGetter, + Function userIdGetter) { + Map grouped = new LinkedHashMap<>(); + for (T reaction : reactions) { + String emoji = emojiGetter.apply(reaction); + MutableReaction mutable = grouped.computeIfAbsent(emoji, key -> new MutableReaction()); + mutable.count++; + if (userId.equals(userIdGetter.apply(reaction))) { + mutable.reactedByMe = true; + } + } + return grouped.entrySet().stream() + .map(entry -> new ReactionResponse(entry.getKey(), entry.getValue().count, entry.getValue().reactedByMe)) + .toList(); + } + + private static class MutableReaction { + private long count; + private boolean reactedByMe; + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java new file mode 100644 index 00000000..c264161f --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java @@ -0,0 +1,131 @@ +package com.aisip.OnO.backend.studyroom.service; + +import com.aisip.OnO.backend.common.exception.ApplicationException; +import com.aisip.OnO.backend.common.response.CursorPageResponse; +import com.aisip.OnO.backend.problem.entity.Problem; +import com.aisip.OnO.backend.problem.entity.ProblemImageData; +import com.aisip.OnO.backend.problem.entity.ProblemImageType; +import com.aisip.OnO.backend.problem.service.ProblemService; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.studyroom.entity.*; +import com.aisip.OnO.backend.studyroom.event.StudyRoomActivityEvent; +import com.aisip.OnO.backend.studyroom.exception.StudyRoomErrorCase; +import com.aisip.OnO.backend.studyroom.repository.*; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.exception.UserErrorCase; +import com.aisip.OnO.backend.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class StudyRoomSharedProblemService { + + private static final Set ALLOWED_SHARED_PROBLEM_EMOJIS = Set.of("😱", "🔥", "👍", "🎉"); + + private final StudyRoomAccessService accessService; + private final StudyRoomSharedProblemRepository sharedProblemRepository; + private final StudyRoomSharedProblemReactionRepository reactionRepository; + private final UserRepository userRepository; + private final ProblemService problemService; + private final StudyRoomReactionService reactionService; + private final ApplicationEventPublisher eventPublisher; + + @Transactional(readOnly = true) + public CursorPageResponse getSharedProblems(Long roomId, Long userId, Long cursor, int size) { + accessService.validateMember(roomId, userId); + int safeSize = Math.min(size, 50); + List sharedProblems = sharedProblemRepository.findByRoomIdAndCursor(roomId, cursor, PageRequest.of(0, safeSize + 1)); + boolean hasNext = sharedProblems.size() > safeSize; + List contentSharedProblems = hasNext ? sharedProblems.subList(0, safeSize) : sharedProblems; + Map> reactions = reactionRepository.findAllBySharedProblemIds( + contentSharedProblems.stream().map(StudyRoomSharedProblem::getId).toList()) + .stream() + .collect(Collectors.groupingBy(reaction -> reaction.getSharedProblem().getId())); + List content = contentSharedProblems.stream() + .map(sharedProblem -> toResponse(sharedProblem, reactions.getOrDefault(sharedProblem.getId(), List.of()), userId)) + .toList(); + Long nextCursor = hasNext && !contentSharedProblems.isEmpty() + ? contentSharedProblems.get(contentSharedProblems.size() - 1).getId() + : null; + return new CursorPageResponse<>(content, nextCursor, hasNext, safeSize); + } + + @Transactional + public SharedProblemResponse shareProblem(Long roomId, Long userId, SharedProblemCreateRequest request) { + accessService.validateMember(roomId, userId); + if (request.problemId() == null || request.comment() != null && request.comment().length() > 100) { + throw new ApplicationException(StudyRoomErrorCase.INVALID_STUDY_ROOM_REQUEST); + } + Problem problem = problemService.findProblemEntityWithImageData(request.problemId(), userId); + User user = userRepository.findById(userId) + .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); + StudyRoomSharedProblem sharedProblem = sharedProblemRepository.save( + StudyRoomSharedProblem.create(accessService.getRoomOrThrow(roomId), user, problem, request.comment())); + eventPublisher.publishEvent(new StudyRoomActivityEvent(userId, StudyRoomFeedEventType.PROBLEM_SHARED, + Map.of("reference", problem.getReference() == null ? "" : problem.getReference(), + "sharedProblemId", sharedProblem.getId()))); + return toResponse(sharedProblem, List.of(), userId); + } + + @Transactional + public SharedProblemReactionToggleResponse toggleReaction(Long roomId, Long sharedProblemId, Long userId, ReactionToggleRequest request) { + accessService.validateMember(roomId, userId); + if (!ALLOWED_SHARED_PROBLEM_EMOJIS.contains(request.emoji())) { + throw new ApplicationException(StudyRoomErrorCase.INVALID_REACTION_EMOJI); + } + StudyRoomSharedProblem sharedProblem = sharedProblemRepository.findByIdAndRoomId(sharedProblemId, roomId) + .orElseThrow(() -> new ApplicationException(StudyRoomErrorCase.SHARED_PROBLEM_NOT_FOUND)); + User user = userRepository.findById(userId) + .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); + reactionRepository.findBySharedProblemIdAndUserIdAndEmoji(sharedProblemId, userId, request.emoji()) + .ifPresentOrElse(reactionRepository::delete, + () -> reactionRepository.save(StudyRoomSharedProblemReaction.create(sharedProblem, user, request.emoji()))); + return new SharedProblemReactionToggleResponse(sharedProblemId, + reactionService.summarizeSharedProblemReactions(reactionRepository.findAllBySharedProblemId(sharedProblemId), userId)); + } + + @Transactional + public void deleteSharedProblem(Long roomId, Long sharedProblemId, Long userId) { + accessService.validateMember(roomId, userId); + StudyRoomSharedProblem sharedProblem = sharedProblemRepository.findByIdAndRoomId(sharedProblemId, roomId) + .orElseThrow(() -> new ApplicationException(StudyRoomErrorCase.SHARED_PROBLEM_NOT_FOUND)); + if (!sharedProblem.getSharedByUser().getId().equals(userId)) { + throw new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_FORBIDDEN); + } + sharedProblemRepository.delete(sharedProblem); + } + + private SharedProblemResponse toResponse(StudyRoomSharedProblem sharedProblem, + List reactions, + Long userId) { + Problem problem = sharedProblem.getProblem(); + return new SharedProblemResponse( + sharedProblem.getId(), + sharedProblem.getSharedByUser().getId(), + sharedProblem.getSharedByUser().getName(), + problem.getId(), + problemImageUrl(problem), + problem.getReference(), + sharedProblem.getComment(), + sharedProblem.getCreatedAt(), + reactionService.summarizeSharedProblemReactions(reactions, userId) + ); + } + + private String problemImageUrl(Problem problem) { + return problem.getProblemImageDataList().stream() + .filter(image -> image.getProblemImageType() == ProblemImageType.PROBLEM_IMAGE) + .map(ProblemImageData::getImageUrl) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudySessionService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudySessionService.java new file mode 100644 index 00000000..4dacba71 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudySessionService.java @@ -0,0 +1,70 @@ +package com.aisip.OnO.backend.studyroom.service; + +import com.aisip.OnO.backend.common.exception.ApplicationException; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.studyroom.entity.StudyRoom; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomFeedEventType; +import com.aisip.OnO.backend.studyroom.entity.StudySession; +import com.aisip.OnO.backend.studyroom.event.StudyRoomActivityEvent; +import com.aisip.OnO.backend.studyroom.exception.StudyRoomErrorCase; +import com.aisip.OnO.backend.studyroom.repository.StudySessionRepository; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.exception.UserErrorCase; +import com.aisip.OnO.backend.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class StudySessionService { + + private final StudyRoomAccessService accessService; + private final StudySessionRepository sessionRepository; + private final UserRepository userRepository; + private final ApplicationEventPublisher eventPublisher; + + @Transactional(readOnly = true) + public ActiveStudySessionsResponse getActiveSessions(Long roomId, Long userId) { + accessService.validateMember(roomId, userId); + return new ActiveStudySessionsResponse(sessionRepository.findActiveByRoomId(roomId).stream() + .map(session -> new ActiveStudySessionResponse( + session.getUser().getId(), session.getUser().getName(), session.getStartedAt())) + .toList()); + } + + @Transactional + public StudySessionStartResponse start(Long roomId, Long userId) { + accessService.validateMember(roomId, userId); + if (!sessionRepository.findActiveByUserIdForUpdate(userId).isEmpty()) { + throw new ApplicationException(StudyRoomErrorCase.SESSION_ALREADY_ACTIVE); + } + StudyRoom room = accessService.getRoomOrThrow(roomId); + User user = userRepository.findById(userId) + .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); + StudySession session = sessionRepository.save(StudySession.start(room, user, LocalDateTime.now())); + eventPublisher.publishEvent(new StudyRoomActivityEvent(userId, StudyRoomFeedEventType.SESSION_STARTED, Map.of())); + return new StudySessionStartResponse(session.getId(), session.getStartedAt()); + } + + @Transactional + public StudySessionEndResponse end(Long roomId, Long sessionId, Long userId) { + accessService.validateMember(roomId, userId); + StudySession session = sessionRepository.findByIdAndRoomIdAndUserId(sessionId, roomId, userId) + .orElseThrow(() -> new ApplicationException(StudyRoomErrorCase.SESSION_NOT_FOUND)); + if (session.getEndedAt() == null) { + session.end(LocalDateTime.now()); + } + return new StudySessionEndResponse(session.getId(), session.getStartedAt(), session.getEndedAt(), session.getDurationMinutes()); + } + + @Transactional + public void closeExpiredSessions(LocalDateTime threshold, LocalDateTime endedAt) { + sessionRepository.findAllByEndedAtIsNullAndStartedAtBefore(threshold) + .forEach(session -> session.end(endedAt)); + } +} From ae753147d3c261ac94a414d04013b82931e8a098 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sat, 6 Jun 2026 23:19:31 +0900 Subject: [PATCH 18/67] =?UTF-8?q?[Feat]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20=EC=A3=BC=EA=B0=84=20=EB=A6=AC=ED=8F=AC=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20=EB=A6=AC=ED=8F=AC=ED=8A=B8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=9D=BD=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=84=B8=EC=85=98=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StudyRoomWeeklyReportController.java | 33 +++ .../studyroom/quartz/StudyRoomScheduler.java | 43 ++++ .../quartz/StudyRoomWeeklyReportJob.java | 19 ++ .../quartz/StudySessionCleanupJob.java | 22 ++ .../service/StudyRoomWeeklyReportService.java | 209 ++++++++++++++++++ 5 files changed, 326 insertions(+) create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomWeeklyReportController.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudyRoomScheduler.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudyRoomWeeklyReportJob.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudySessionCleanupJob.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomWeeklyReportService.java diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomWeeklyReportController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomWeeklyReportController.java new file mode 100644 index 00000000..1e7ff0c4 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomWeeklyReportController.java @@ -0,0 +1,33 @@ +package com.aisip.OnO.backend.studyroom.controller; + +import com.aisip.OnO.backend.common.response.CommonResponse; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.studyroom.service.StudyRoomWeeklyReportService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/study-room/{roomId}/weekly-reports") +public class StudyRoomWeeklyReportController { + + private final StudyRoomWeeklyReportService reportService; + + @GetMapping + public CommonResponse> getReports(@PathVariable Long roomId, + @RequestParam(defaultValue = "4") int limit) { + return CommonResponse.success(reportService.getReports(roomId, currentUserId(), limit)); + } + + @PatchMapping("/{reportId}/read") + public CommonResponse markRead(@PathVariable Long roomId, @PathVariable Long reportId) { + return CommonResponse.success(reportService.markRead(roomId, reportId, currentUserId())); + } + + private Long currentUserId() { + return (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudyRoomScheduler.java b/src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudyRoomScheduler.java new file mode 100644 index 00000000..40ebf410 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudyRoomScheduler.java @@ -0,0 +1,43 @@ +package com.aisip.OnO.backend.studyroom.quartz; + +import org.quartz.*; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class StudyRoomScheduler { + + @Bean + public JobDetail studyRoomWeeklyReportJobDetail() { + return JobBuilder.newJob(StudyRoomWeeklyReportJob.class) + .withIdentity("studyRoomWeeklyReportJob") + .storeDurably() + .build(); + } + + @Bean + public Trigger studyRoomWeeklyReportTrigger() { + return TriggerBuilder.newTrigger() + .forJob(studyRoomWeeklyReportJobDetail()) + .withIdentity("studyRoomWeeklyReportTrigger") + .withSchedule(CronScheduleBuilder.cronSchedule("0 0 8 ? * MON").inTimeZone(java.util.TimeZone.getTimeZone("Asia/Seoul"))) + .build(); + } + + @Bean + public JobDetail studySessionCleanupJobDetail() { + return JobBuilder.newJob(StudySessionCleanupJob.class) + .withIdentity("studySessionCleanupJob") + .storeDurably() + .build(); + } + + @Bean + public Trigger studySessionCleanupTrigger() { + return TriggerBuilder.newTrigger() + .forJob(studySessionCleanupJobDetail()) + .withIdentity("studySessionCleanupTrigger") + .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInHours(1).repeatForever()) + .build(); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudyRoomWeeklyReportJob.java b/src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudyRoomWeeklyReportJob.java new file mode 100644 index 00000000..041b9da5 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudyRoomWeeklyReportJob.java @@ -0,0 +1,19 @@ +package com.aisip.OnO.backend.studyroom.quartz; + +import com.aisip.OnO.backend.studyroom.service.StudyRoomWeeklyReportService; +import lombok.RequiredArgsConstructor; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class StudyRoomWeeklyReportJob implements Job { + + private final StudyRoomWeeklyReportService reportService; + + @Override + public void execute(JobExecutionContext context) { + reportService.createPreviousWeekReports(); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudySessionCleanupJob.java b/src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudySessionCleanupJob.java new file mode 100644 index 00000000..d6de021b --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudySessionCleanupJob.java @@ -0,0 +1,22 @@ +package com.aisip.OnO.backend.studyroom.quartz; + +import com.aisip.OnO.backend.studyroom.service.StudySessionService; +import lombok.RequiredArgsConstructor; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Component +@RequiredArgsConstructor +public class StudySessionCleanupJob implements Job { + + private final StudySessionService sessionService; + + @Override + public void execute(JobExecutionContext context) { + LocalDateTime now = LocalDateTime.now(); + sessionService.closeExpiredSessions(now.minusHours(12), now); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomWeeklyReportService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomWeeklyReportService.java new file mode 100644 index 00000000..fb8fd6f6 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomWeeklyReportService.java @@ -0,0 +1,209 @@ +package com.aisip.OnO.backend.studyroom.service; + +import com.aisip.OnO.backend.common.exception.ApplicationException; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomStats; +import com.aisip.OnO.backend.studyroom.entity.*; +import com.aisip.OnO.backend.studyroom.exception.StudyRoomErrorCase; +import com.aisip.OnO.backend.studyroom.repository.*; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.exception.UserErrorCase; +import com.aisip.OnO.backend.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.temporal.TemporalAdjusters; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class StudyRoomWeeklyReportService { + + private static final ZoneId KST = ZoneId.of("Asia/Seoul"); + private static final String DEFAULT_CHEER_MESSAGE = "이번 주도 모두 고생했어요!"; + private static final int REPORT_ROOM_BATCH_SIZE = 100; + + private final StudyRoomAccessService accessService; + private final StudyRoomRepository roomRepository; + private final StudyRoomMemberRepository memberRepository; + private final StudyRoomWeeklyReportRepository reportRepository; + private final StudyRoomWeeklyReportReadRepository readRepository; + private final StudyRoomChallengeRepository challengeRepository; + private final StudyRoomStatsService statsService; + private final UserRepository userRepository; + + @Transactional(readOnly = true) + public List getReports(Long roomId, Long userId, int limit) { + accessService.validateMember(roomId, userId); + int safeLimit = Math.max(1, Math.min(limit, 12)); + List reports = reportRepository.findAllByRoomIdOrderByWeekStartDesc(roomId, PageRequest.of(0, safeLimit)); + Set readReportIds = readRepository.findAllByReportIdsAndUserId( + reports.stream().map(StudyRoomWeeklyReport::getId).toList(), userId) + .stream() + .map(read -> read.getReport().getId()) + .collect(Collectors.toSet()); + return reports.stream().map(report -> toResponse(report, readReportIds.contains(report.getId()))).toList(); + } + + @Transactional + public WeeklyReportReadResponse markRead(Long roomId, Long reportId, Long userId) { + accessService.validateMember(roomId, userId); + StudyRoomWeeklyReport report = reportRepository.findByIdAndRoomId(reportId, roomId) + .orElseThrow(() -> new ApplicationException(StudyRoomErrorCase.REPORT_NOT_FOUND)); + if (readRepository.findByReportIdAndUserId(reportId, userId).isEmpty()) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); + readRepository.save(StudyRoomWeeklyReportRead.create(report, user, LocalDateTime.now())); + } + return new WeeklyReportReadResponse(reportId, true); + } + + @Transactional + public void createPreviousWeekReports() { + LocalDate today = LocalDate.now(KST); + LocalDate weekStart = today.with(TemporalAdjusters.previous(DayOfWeek.MONDAY)); + LocalDate weekEnd = weekStart.plusDays(6); + LocalDateTime start = weekStart.atStartOfDay(); + LocalDateTime end = weekEnd.atTime(LocalTime.MAX); + Long cursor = 0L; + while (true) { + List rooms = roomRepository.findBatchAfterId(cursor, PageRequest.of(0, REPORT_ROOM_BATCH_SIZE)); + if (rooms.isEmpty()) { + return; + } + createReportsForBatch(rooms, weekStart, weekEnd, start, end); + cursor = rooms.get(rooms.size() - 1).getId(); + } + } + + void createReportIfAbsent(StudyRoom room, LocalDate weekStart, LocalDate weekEnd, LocalDateTime start, LocalDateTime end) { + List members = memberRepository.findAllWithUserByRoomId(room.getId()); + List userIds = members.stream().map(member -> member.getUser().getId()).toList(); + Map stats = statsService.getStats(userIds, start, end, weekEnd); + createReportIfAbsent(room, members, stats, weekStart, weekEnd, 0); + } + + private void createReportsForBatch(List rooms, LocalDate weekStart, LocalDate weekEnd, + LocalDateTime start, LocalDateTime end) { + List roomIds = rooms.stream().map(StudyRoom::getId).toList(); + List members = memberRepository.findAllWithRoomAndUserByRoomIds(roomIds); + Map> membersByRoomId = members.stream() + .collect(Collectors.groupingBy(member -> member.getRoom().getId(), LinkedHashMap::new, Collectors.toList())); + List userIds = members.stream().map(member -> member.getUser().getId()).distinct().toList(); + Map streaks = statsService.currentStreaks(userIds, weekEnd); + Map statsByUserId = statsService.getStats(userIds, start, end, streaks); + refreshChallengeStatuses(roomIds, membersByRoomId); + Map completedChallengeCounts = completedChallengeCounts(roomIds, start, end); + + for (StudyRoom room : rooms) { + List roomMembers = membersByRoomId.getOrDefault(room.getId(), List.of()); + Map roomStats = roomMembers.stream() + .collect(Collectors.toMap( + member -> member.getUser().getId(), + member -> statsByUserId.getOrDefault(member.getUser().getId(), new StudyRoomStats(0, 0, 0)) + )); + createReportIfAbsent(room, roomMembers, roomStats, weekStart, weekEnd, + completedChallengeCounts.getOrDefault(room.getId(), 0)); + } + } + + private void createReportIfAbsent(StudyRoom room, List members, Map stats, + LocalDate weekStart, LocalDate weekEnd, int challengesCompleted) { + if (reportRepository.existsByRoomIdAndWeekStart(room.getId(), weekStart)) { + return; + } + StudyRoomMember topProblemMember = members.stream() + .max(Comparator.comparingInt(member -> stats.getOrDefault(member.getUser().getId(), new StudyRoomStats(0, 0, 0)).weeklyProblemCount())) + .orElse(null); + StudyRoomMember topStreakMember = members.stream() + .max(Comparator.comparingInt(member -> stats.getOrDefault(member.getUser().getId(), new StudyRoomStats(0, 0, 0)).currentStreak())) + .orElse(null); + int topProblems = topProblemMember == null ? 0 : stats.getOrDefault(topProblemMember.getUser().getId(), new StudyRoomStats(0, 0, 0)).weeklyProblemCount(); + int longestStreak = topStreakMember == null ? 0 : stats.getOrDefault(topStreakMember.getUser().getId(), new StudyRoomStats(0, 0, 0)).currentStreak(); + int totalProblems = stats.values().stream().mapToInt(StudyRoomStats::weeklyProblemCount).sum(); + reportRepository.save(StudyRoomWeeklyReport.create( + room, + weekStart, + weekEnd, + topProblemMember == null ? null : topProblemMember.getUser().getName(), + topProblems, + topStreakMember == null ? null : topStreakMember.getUser().getName(), + longestStreak, + totalProblems, + challengesCompleted, + DEFAULT_CHEER_MESSAGE + )); + } + + private Map completedChallengeCounts(List roomIds, LocalDateTime start, LocalDateTime end) { + Map result = new HashMap<>(); + challengeRepository.countByRoomIdsAndStatusAndEndAtBetween(roomIds, StudyRoomChallengeStatus.COMPLETED, start, end) + .forEach(row -> result.put((Long) row[0], Math.toIntExact((Long) row[1]))); + return result; + } + + private void refreshChallengeStatuses(List roomIds, Map> membersByRoomId) { + List challenges = challengeRepository.findAllByRoomIdsAndStatus(roomIds, StudyRoomChallengeStatus.IN_PROGRESS); + for (StudyRoomChallenge challenge : challenges) { + List members = membersByRoomId.getOrDefault(challenge.getRoom().getId(), List.of()); + List userIds = members.stream().map(member -> member.getUser().getId()).toList(); + Map streaks = statsService.currentStreaks( + userIds, + min(LocalDate.now(KST), challenge.getEndAt().toLocalDate()), + challenge.getStartAt().toLocalDate() + ); + Map stats = statsService.getStats(userIds, challenge.getStartAt(), challenge.getEndAt(), streaks); + if (isChallengeCompleted(challenge, members, stats)) { + challenge.updateStatus(StudyRoomChallengeStatus.COMPLETED); + continue; + } + if (challenge.getEndAt().isBefore(LocalDateTime.now())) { + challenge.updateStatus(StudyRoomChallengeStatus.EXPIRED); + } + } + } + + private boolean isChallengeCompleted(StudyRoomChallenge challenge, List members, + Map stats) { + if (members.isEmpty()) { + return false; + } + if (challenge.getType() == StudyRoomChallengeType.GROUP) { + int groupCurrent = stats.values().stream() + .mapToInt(stat -> metricValue(challenge.getMetric(), stat)) + .sum(); + return groupCurrent >= challenge.getTargetValue(); + } + return members.stream() + .allMatch(member -> metricValue(challenge.getMetric(), + stats.getOrDefault(member.getUser().getId(), new StudyRoomStats(0, 0, 0))) >= challenge.getTargetValue()); + } + + private int metricValue(StudyRoomChallengeMetric metric, StudyRoomStats stats) { + return switch (metric) { + case WEEKLY_PROBLEM_COUNT -> stats.weeklyProblemCount(); + case WEEKLY_PRACTICE_COUNT -> stats.weeklyPracticeCount(); + case STREAK -> stats.currentStreak(); + }; + } + + private LocalDate min(LocalDate left, LocalDate right) { + return left.isBefore(right) ? left : right; + } + + private WeeklyReportResponse toResponse(StudyRoomWeeklyReport report, boolean read) { + return new WeeklyReportResponse(report.getId(), report.getWeekStart(), report.getWeekEnd(), + report.getTopMemberName(), report.getTopMemberProblemCount(), + report.getLongestStreakName(), report.getLongestStreakDays(), + report.getTotalProblems(), report.getChallengesCompleted(), + report.getCheerMessage(), read); + } +} From fa4ebe2bafd7b7d1e1960f6b8dbd3083eb569705 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sat, 6 Jun 2026 23:19:50 +0900 Subject: [PATCH 19/67] =?UTF-8?q?[Feat]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20=ED=99=9C=EB=8F=99=20=EC=97=B0=EB=8F=99=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20=EB=AC=B8=EC=A0=9C=20=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?=ED=92=80=EC=9D=B4=20=EC=99=84=EB=A3=8C=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/config/SecurityConfig.java | 1 + .../problem/service/ProblemService.java | 10 ++ .../service/ProblemSolveService.java | 6 + .../user/repository/UserRepository.java | 6 + .../StudyRoomServiceIntegrationTest.java | 159 ++++++++++++++++++ 5 files changed, 182 insertions(+) create mode 100644 src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java diff --git a/src/main/java/com/aisip/OnO/backend/auth/config/SecurityConfig.java b/src/main/java/com/aisip/OnO/backend/auth/config/SecurityConfig.java index 7643715b..854d340c 100644 --- a/src/main/java/com/aisip/OnO/backend/auth/config/SecurityConfig.java +++ b/src/main/java/com/aisip/OnO/backend/auth/config/SecurityConfig.java @@ -106,6 +106,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/api/folders/**").hasAnyRole("GUEST", "MEMBER", "ADMIN") .requestMatchers("/api/fileUpload/**").hasAnyRole("GUEST", "MEMBER", "ADMIN") .requestMatchers("/api/practiceNotes/**").hasAnyRole("GUEST", "MEMBER", "ADMIN") + .requestMatchers("/api/study-room/**").hasAnyRole("GUEST", "MEMBER", "ADMIN") .anyRequest().authenticated() ) .formLogin(formLogin -> formLogin diff --git a/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java b/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java index 16fdd62a..6d1a3459 100644 --- a/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java +++ b/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java @@ -30,6 +30,8 @@ import com.aisip.OnO.backend.practicenote.repository.PracticeNoteRepository; import com.aisip.OnO.backend.problemsolve.repository.ProblemSolveRepository; import com.aisip.OnO.backend.problemsolve.repository.ProblemSolveSummary; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomFeedEventType; +import com.aisip.OnO.backend.studyroom.event.StudyRoomActivityEvent; import com.aisip.OnO.backend.tag.entity.ProblemTagMapping; import com.aisip.OnO.backend.tag.entity.Tag; import com.aisip.OnO.backend.tag.exception.TagErrorCase; @@ -38,6 +40,7 @@ import com.aisip.OnO.backend.util.redis.StreakCacheService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.data.domain.Page; @@ -88,6 +91,7 @@ public class ProblemService { private final ProblemTagMappingRepository problemTagMappingRepository; private final RateLimitService rateLimitService; private final StreakCacheService streakCacheService; + private final ApplicationEventPublisher eventPublisher; @Transactional(readOnly = true) public ProblemResponseDto findProblemForAdmin(Long problemId) { @@ -214,6 +218,8 @@ public Long registerProblem(ProblemRegisterDto problemRegisterDto, Long userId) analysisService.createSkippedAnalysis(problem.getId()); streakCacheService.evict(userId); missionLogService.registerProblemWriteMission(userId); + eventPublisher.publishEvent(new StudyRoomActivityEvent( + userId, StudyRoomFeedEventType.PROBLEM_REGISTERED, Map.of("count", 1))); log.info("userId: {} register problemId: {}", userId, problem.getId()); @@ -271,6 +277,8 @@ public Long registerProblemV2(ProblemRegisterV2Dto problemRegisterV2Dto, Long us analysisService.createSkippedAnalysis(problem.getId()); streakCacheService.evict(userId); missionLogService.registerProblemWriteMission(userId); + eventPublisher.publishEvent(new StudyRoomActivityEvent( + userId, StudyRoomFeedEventType.PROBLEM_REGISTERED, Map.of("count", 1))); log.info("userId: {} register problem(v2) problemId: {}", userId, problem.getId()); return problem.getId(); @@ -333,6 +341,8 @@ public List registerProblemsV2(ProblemRegisterV2BatchDto problemRegisterV2 streakCacheService.evict(userId); missionLogService.registerProblemWriteMissionBatch(userId, problems.size()); + eventPublisher.publishEvent(new StudyRoomActivityEvent( + userId, StudyRoomFeedEventType.PROBLEM_REGISTERED, Map.of("count", problems.size()))); List problemIds = problems.stream() .map(Problem::getId) diff --git a/src/main/java/com/aisip/OnO/backend/problemsolve/service/ProblemSolveService.java b/src/main/java/com/aisip/OnO/backend/problemsolve/service/ProblemSolveService.java index e4380ad7..fb0b053e 100644 --- a/src/main/java/com/aisip/OnO/backend/problemsolve/service/ProblemSolveService.java +++ b/src/main/java/com/aisip/OnO/backend/problemsolve/service/ProblemSolveService.java @@ -15,6 +15,8 @@ import com.aisip.OnO.backend.problem.entity.Problem; import com.aisip.OnO.backend.problem.exception.ProblemErrorCase; import com.aisip.OnO.backend.problem.repository.ProblemRepository; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomFeedEventType; +import com.aisip.OnO.backend.studyroom.event.StudyRoomActivityEvent; import com.aisip.OnO.backend.util.fileupload.service.FileUploadService; import com.aisip.OnO.backend.config.rabbitmq.producer.S3DeleteProducer; import com.fasterxml.jackson.databind.ObjectMapper; @@ -22,6 +24,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.web.multipart.MultipartFile; import java.util.ArrayList; @@ -46,6 +49,7 @@ public class ProblemSolveService { private final S3DeleteProducer s3DeleteProducer; private final ObjectMapper objectMapper; private final StreakCacheService streakCacheService; + private final ApplicationEventPublisher eventPublisher; @Qualifier("s3UploadExecutor") private final Executor s3UploadExecutor; @@ -135,6 +139,8 @@ public Long createProblemSolve(ProblemSolveRegisterDto dto, Long userId) { problem.getConsecutiveCorrectCount() ); problem.updateReviewSchedule(schedule.nextReviewAt(), schedule.reviewInterval(), schedule.consecutiveCorrectCount()); + eventPublisher.publishEvent(new StudyRoomActivityEvent( + userId, StudyRoomFeedEventType.PRACTICE_COMPLETED, java.util.Map.of())); log.info("userId: {} created problem solve: {}, nextReviewAt: {}, mastered: {}", userId, problemSolve.getId(), schedule.nextReviewAt(), schedule.isMastered()); diff --git a/src/main/java/com/aisip/OnO/backend/user/repository/UserRepository.java b/src/main/java/com/aisip/OnO/backend/user/repository/UserRepository.java index 8ac86f0f..b328c3fb 100644 --- a/src/main/java/com/aisip/OnO/backend/user/repository/UserRepository.java +++ b/src/main/java/com/aisip/OnO/backend/user/repository/UserRepository.java @@ -4,6 +4,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import jakarta.persistence.LockModeType; +import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -22,6 +24,10 @@ public interface UserRepository extends JpaRepository, UserRepositor Optional findByName(String name); + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select u from User u where u.id = :userId") + Optional findByIdForUpdate(@Param("userId") Long userId); + Page findAll(Pageable pageable); List findAllByCreatedAtBetweenOrderByCreatedAtDesc(LocalDateTime startDateTime, LocalDateTime endDateTime); diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java new file mode 100644 index 00000000..9d62cdd8 --- /dev/null +++ b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java @@ -0,0 +1,159 @@ +package com.aisip.OnO.backend.studyroom.service; + +import com.aisip.OnO.backend.common.exception.ApplicationException; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.InviteCodeResponse; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.StudyRoomCreateRequest; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.StudyRoomDetailResponse; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.StudyRoomGoalUpdateRequest; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.StudyRoomJoinRequest; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomFeed; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomFeedEventType; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomFeedReaction; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomMemberRole; +import com.aisip.OnO.backend.studyroom.exception.StudyRoomErrorCase; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomFeedReactionRepository; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomFeedRepository; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomRepository; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomMemberRepository; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.repository.UserRepository; +import com.aisip.OnO.backend.util.RandomUserGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest +class StudyRoomServiceIntegrationTest { + + @Autowired + private StudyRoomService studyRoomService; + + @Autowired + private StudyRoomInviteService inviteService; + + @Autowired + private StudyRoomMemberRepository memberRepository; + + @Autowired + private StudyRoomRepository roomRepository; + + @Autowired + private StudyRoomFeedRepository feedRepository; + + @Autowired + private StudyRoomFeedReactionRepository feedReactionRepository; + + @Autowired + private UserRepository userRepository; + + @Test + void createRoomCreatesHostMember() { + User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); + + StudyRoomDetailResponse response = studyRoomService.createRoom(new StudyRoomCreateRequest("수능 준비방"), host.getId()); + + assertThat(response.name()).isEqualTo("수능 준비방"); + assertThat(response.hostUserId()).isEqualTo(host.getId()); + assertThat(response.memberCount()).isEqualTo(1); + assertThat(memberRepository.existsByRoomIdAndUserId(response.roomId(), host.getId())).isTrue(); + } + + @Test + void joinWithInviteCodeAddsMember() { + User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); + User member = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "멤버", "member")); + StudyRoomDetailResponse room = studyRoomService.createRoom(new StudyRoomCreateRequest("수능 준비방"), host.getId()); + InviteCodeResponse invite = inviteService.issueInviteCode(room.roomId(), host.getId()); + + StudyRoomDetailResponse joined = inviteService.join(new StudyRoomJoinRequest(invite.code()), member.getId()); + + assertThat(joined.memberCount()).isEqualTo(2); + assertThat(memberRepository.existsByRoomIdAndUserId(room.roomId(), member.getId())).isTrue(); + } + + @Test + void memberCanRejoinAfterLeavingRoom() { + User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); + User member = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "멤버", "member")); + StudyRoomDetailResponse room = studyRoomService.createRoom(new StudyRoomCreateRequest("수능 준비방"), host.getId()); + InviteCodeResponse invite = inviteService.issueInviteCode(room.roomId(), host.getId()); + inviteService.join(new StudyRoomJoinRequest(invite.code()), member.getId()); + + studyRoomService.leaveRoom(room.roomId(), member.getId()); + StudyRoomDetailResponse rejoined = inviteService.join(new StudyRoomJoinRequest(invite.code()), member.getId()); + + assertThat(rejoined.memberCount()).isEqualTo(2); + assertThat(memberRepository.existsByRoomIdAndUserId(room.roomId(), member.getId())).isTrue(); + } + + @Test + void feedReactionCanBeRecreatedAfterDelete() { + User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); + StudyRoomDetailResponse room = studyRoomService.createRoom(new StudyRoomCreateRequest("수능 준비방"), host.getId()); + StudyRoomFeed feed = feedRepository.save(StudyRoomFeed.create( + roomRepository.findById(room.roomId()).orElseThrow(), + host, + StudyRoomFeedEventType.SESSION_STARTED, + "{}" + )); + StudyRoomFeedReaction reaction = feedReactionRepository.save(StudyRoomFeedReaction.create(feed, host, "🔥")); + + feedReactionRepository.delete(reaction); + feedReactionRepository.flush(); + StudyRoomFeedReaction recreated = feedReactionRepository.saveAndFlush(StudyRoomFeedReaction.create(feed, host, "🔥")); + + assertThat(recreated.getId()).isNotNull(); + } + + @Test + void nonMemberCannotReadRoom() { + User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); + User outsider = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "외부", "outsider")); + StudyRoomDetailResponse room = studyRoomService.createRoom(new StudyRoomCreateRequest("수능 준비방"), host.getId()); + + assertThatThrownBy(() -> studyRoomService.getRoom(room.roomId(), outsider.getId())) + .isInstanceOf(ApplicationException.class) + .extracting("errorCase") + .isEqualTo(StudyRoomErrorCase.STUDY_ROOM_FORBIDDEN); + } + + @Test + void hostLeaveTransfersHostToNextMember() { + User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); + User member = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "멤버", "member")); + StudyRoomDetailResponse room = studyRoomService.createRoom(new StudyRoomCreateRequest("수능 준비방"), host.getId()); + InviteCodeResponse invite = inviteService.issueInviteCode(room.roomId(), host.getId()); + inviteService.join(new StudyRoomJoinRequest(invite.code()), member.getId()); + + studyRoomService.leaveRoom(room.roomId(), host.getId()); + + assertThat(memberRepository.existsByRoomIdAndUserId(room.roomId(), host.getId())).isFalse(); + assertThat(memberRepository.findByRoomIdAndUserId(room.roomId(), member.getId()).orElseThrow().getRole()) + .isEqualTo(StudyRoomMemberRole.HOST); + assertThat(roomRepository.findById(room.roomId()).orElseThrow().getHostUserId()).isEqualTo(member.getId()); + } + + @Test + void hostLeaveDeletesRoomWhenNoOtherMemberExists() { + User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); + StudyRoomDetailResponse room = studyRoomService.createRoom(new StudyRoomCreateRequest("수능 준비방"), host.getId()); + + studyRoomService.leaveRoom(room.roomId(), host.getId()); + + assertThat(roomRepository.findById(room.roomId())).isEmpty(); + } + + @Test + void updateGoalCanSetAndClearWeeklyGoal() { + User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); + StudyRoomDetailResponse room = studyRoomService.createRoom(new StudyRoomCreateRequest("수능 준비방"), host.getId()); + + assertThat(studyRoomService.updateGoal(room.roomId(), host.getId(), new StudyRoomGoalUpdateRequest(20)).weeklyGoal()) + .isEqualTo(20); + assertThat(studyRoomService.updateGoal(room.roomId(), host.getId(), new StudyRoomGoalUpdateRequest(0)).weeklyGoal()) + .isNull(); + } +} From fe5c50498580cb67563323b64093a808943cb7f3 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sun, 7 Jun 2026 14:18:31 +0900 Subject: [PATCH 20/67] =?UTF-8?q?[Feat]=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EC=9D=B4=EB=AA=A8=EC=A7=80=20=EA=B3=B5=ED=86=B5=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EA=B8=B0=EB=B0=98=20=EC=B6=94=EA=B0=80=20-=20?= =?UTF-8?q?=ED=97=88=EC=9A=A9=20=EC=9D=B4=EB=AA=A8=EC=A7=80=20=ED=82=A4=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=EA=B3=BC=20=EA=B2=80=EC=A6=9D=EA=B8=B0,=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98=20-=20=EB=B3=B5=EC=8A=B5=EB=85=B8=ED=8A=B8=20?= =?UTF-8?q?=EC=86=8C=EA=B0=90=20=EC=BB=AC=EB=9F=BC=C2=B7=ED=95=99=EC=8A=B5?= =?UTF-8?q?=20=EC=BA=98=EB=A6=B0=EB=8D=94=20=EA=B0=90=EC=A0=95=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EC=B6=94=EA=B0=80=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98(V7)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/emoji/CustomEmojiErrorCase.java | 16 +++ .../common/emoji/CustomEmojiValidator.java | 98 +++++++++++++++++++ .../migration/V7__add_custom_emoji_fields.sql | 16 +++ 3 files changed, 130 insertions(+) create mode 100644 src/main/java/com/aisip/OnO/backend/common/emoji/CustomEmojiErrorCase.java create mode 100644 src/main/java/com/aisip/OnO/backend/common/emoji/CustomEmojiValidator.java create mode 100644 src/main/resources/db/migration/V7__add_custom_emoji_fields.sql diff --git a/src/main/java/com/aisip/OnO/backend/common/emoji/CustomEmojiErrorCase.java b/src/main/java/com/aisip/OnO/backend/common/emoji/CustomEmojiErrorCase.java new file mode 100644 index 00000000..03e68324 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/common/emoji/CustomEmojiErrorCase.java @@ -0,0 +1,16 @@ +package com.aisip.OnO.backend.common.emoji; + +import com.aisip.OnO.backend.common.exception.ErrorCase; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum CustomEmojiErrorCase implements ErrorCase { + + INVALID_EMOJI_KEY(400, 11001, "지원하지 않는 이모지입니다."); + + private final Integer httpStatusCode; + private final Integer errorCode; + private final String message; +} diff --git a/src/main/java/com/aisip/OnO/backend/common/emoji/CustomEmojiValidator.java b/src/main/java/com/aisip/OnO/backend/common/emoji/CustomEmojiValidator.java new file mode 100644 index 00000000..c858f376 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/common/emoji/CustomEmojiValidator.java @@ -0,0 +1,98 @@ +package com.aisip.OnO.backend.common.emoji; + +import com.aisip.OnO.backend.common.exception.ApplicationException; +import org.springframework.stereotype.Component; + +import java.util.Set; + +@Component +public class CustomEmojiValidator { + + private static final Set ALLOWED_KEYS = Set.of( + "angry_with_flames", + "crying_in_rain", + "confused_question", + "cool_sunglasses", + "happy_tears", + "excited_happy", + "lol_laughing_tears", + "cheek_to_cheek", + "dizzy_spiral_eyes2", + "stressed_bomb", + "shy_clasped_hands", + "shy_wink_heart", + "in_love_heart", + "scared_dread", + "star_eyes_excited", + "frustrated_studying", + "holding_pen", + "reading_with_glasses", + "reading_tablet", + "studying_together", + "studying_with_lamp", + "idea_lightbulb", + "got_100_score", + "success_checkmark", + "writing_wink", + "working_laptop", + "birthday_cake", + "celebrating_confetti", + "cheering_megaphone", + "excited_with_gift", + "fired_up_sparkle_eyes", + "superhero_cape", + "gold_medal", + "holding_flower", + "holding_heart", + "lifting_weights", + "party_celebration", + "praying_grateful", + "sprout_growth", + "trophy_celebration", + "thumbs_up_happy", + "thumbs_up_wink", + "thank_you_sign", + "winking_fist", + "hi_greeting", + "waving_hello", + "cheers_beer", + "cuddle_love", + "texting_heart", + "puzzle_teamwork", + "peeking_wall", + "blue_hoodie", + "sleeping_blanket", + "cozy_blanket", + "cooking_chef", + "drinking_coffee", + "sleeping_with_star", + "eating_ice_cream", + "eating_skewer", + "playing_game_controller", + "hiking_backpack", + "listening_headphones", + "morning_coffee", + "shopping_bags", + "singing_happy", + "taking_bath", + "taking_photo", + "umbrella_rain", + "wearing_scarf" + ); + + public void validate(String emojiKey) { + if (!isAllowed(emojiKey)) { + throw new ApplicationException(CustomEmojiErrorCase.INVALID_EMOJI_KEY); + } + } + + public void validateNullable(String emojiKey) { + if (emojiKey != null) { + validate(emojiKey); + } + } + + public boolean isAllowed(String emojiKey) { + return emojiKey != null && ALLOWED_KEYS.contains(emojiKey); + } +} diff --git a/src/main/resources/db/migration/V7__add_custom_emoji_fields.sql b/src/main/resources/db/migration/V7__add_custom_emoji_fields.sql new file mode 100644 index 00000000..0290ae10 --- /dev/null +++ b/src/main/resources/db/migration/V7__add_custom_emoji_fields.sql @@ -0,0 +1,16 @@ +ALTER TABLE practice_note + ADD COLUMN last_session_mood_emoji_key VARCHAR(80) NULL; + +CREATE TABLE learning_calendar_mood ( + id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + study_date DATE NOT NULL, + emoji_key VARCHAR(80) NOT NULL, + created_at DATETIME(6), + updated_at DATETIME(6), + deleted_at DATETIME(6), + PRIMARY KEY (id), + CONSTRAINT uk_learning_calendar_mood_user_date UNIQUE (user_id, study_date), + CONSTRAINT fk_learning_calendar_mood_user FOREIGN KEY (user_id) REFERENCES user (id) +); +CREATE INDEX idx_learning_calendar_mood_user_date ON learning_calendar_mood (user_id, study_date); From 76de34f488466b6d06d85e63d8403de5ec9c9631 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sun, 7 Jun 2026 14:18:39 +0900 Subject: [PATCH 21/67] =?UTF-8?q?[Feat]=20=ED=95=99=EC=8A=B5=20=EC=BA=98?= =?UTF-8?q?=EB=A6=B0=EB=8D=94=20=EA=B0=90=EC=A0=95=20=EC=9D=B4=EB=AA=A8?= =?UTF-8?q?=EC=A7=80=20=EA=B8=B0=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20=ED=95=99=EC=8A=B5=20=EA=B8=B0=EB=A1=9D?= =?UTF-8?q?=EC=9D=B4=20=EC=9E=88=EB=8A=94=20=EB=82=A0=EC=A7=9C=EC=97=90=20?= =?UTF-8?q?=ED=95=9C=ED=95=B4=20=EA=B0=90=EC=A0=95=20=EC=9D=B4=EB=AA=A8?= =?UTF-8?q?=EC=A7=80(mood)=20=EB=93=B1=EB=A1=9D/=EC=88=98=EC=A0=95=20PATCH?= =?UTF-8?q?=20API=20=EC=B6=94=EA=B0=80=20-=20=EC=BA=98=EB=A6=B0=EB=8D=94?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=EC=97=90=20moodEmojiKey=20=ED=8F=AC?= =?UTF-8?q?=ED=95=A8,=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC/=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B3=B4?= =?UTF-8?q?=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LearningCalendarController.java | 10 ++++ .../dto/LearningCalendarMoodRequestDto.java | 9 ++++ .../dto/LearningCalendarMoodResponseDto.java | 9 ++++ .../dto/LearningCalendarResponseDto.java | 3 +- .../entity/LearningCalendarMood.java | 52 +++++++++++++++++++ .../exception/LearningCalendarErrorCase.java | 17 ++++++ .../LearningCalendarMoodRepository.java | 15 ++++++ .../LearningCalendarQueryRepository.java | 21 ++++++++ .../service/LearningCalendarService.java | 34 +++++++++++- .../LearningCalendarControllerTest.java | 30 ++++++++++- .../service/LearningCalendarServiceTest.java | 40 ++++++++++++++ 11 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/aisip/OnO/backend/learningcalendar/dto/LearningCalendarMoodRequestDto.java create mode 100644 src/main/java/com/aisip/OnO/backend/learningcalendar/dto/LearningCalendarMoodResponseDto.java create mode 100644 src/main/java/com/aisip/OnO/backend/learningcalendar/entity/LearningCalendarMood.java create mode 100644 src/main/java/com/aisip/OnO/backend/learningcalendar/exception/LearningCalendarErrorCase.java create mode 100644 src/main/java/com/aisip/OnO/backend/learningcalendar/repository/LearningCalendarMoodRepository.java diff --git a/src/main/java/com/aisip/OnO/backend/learningcalendar/controller/LearningCalendarController.java b/src/main/java/com/aisip/OnO/backend/learningcalendar/controller/LearningCalendarController.java index 90a72f68..1605a70b 100644 --- a/src/main/java/com/aisip/OnO/backend/learningcalendar/controller/LearningCalendarController.java +++ b/src/main/java/com/aisip/OnO/backend/learningcalendar/controller/LearningCalendarController.java @@ -1,12 +1,16 @@ package com.aisip.OnO.backend.learningcalendar.controller; import com.aisip.OnO.backend.common.response.CommonResponse; +import com.aisip.OnO.backend.learningcalendar.dto.LearningCalendarMoodRequestDto; +import com.aisip.OnO.backend.learningcalendar.dto.LearningCalendarMoodResponseDto; import com.aisip.OnO.backend.learningcalendar.dto.LearningCalendarResponseDto; import com.aisip.OnO.backend.learningcalendar.service.LearningCalendarService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +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; @@ -29,6 +33,12 @@ public CommonResponse getLearningCalendar( return CommonResponse.success(learningCalendarService.getLearningCalendar(userId, year, month)); } + @PatchMapping("/mood") + public CommonResponse updateMood(@RequestBody LearningCalendarMoodRequestDto request) { + Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + return CommonResponse.success(learningCalendarService.updateMood(userId, request)); + } + private void validateYearMonth(int year, int month) { if (year < 1 || month < 1 || month > 12) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."); diff --git a/src/main/java/com/aisip/OnO/backend/learningcalendar/dto/LearningCalendarMoodRequestDto.java b/src/main/java/com/aisip/OnO/backend/learningcalendar/dto/LearningCalendarMoodRequestDto.java new file mode 100644 index 00000000..ba103e33 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/learningcalendar/dto/LearningCalendarMoodRequestDto.java @@ -0,0 +1,9 @@ +package com.aisip.OnO.backend.learningcalendar.dto; + +import java.time.LocalDate; + +public record LearningCalendarMoodRequestDto( + LocalDate date, + String emojiKey +) { +} diff --git a/src/main/java/com/aisip/OnO/backend/learningcalendar/dto/LearningCalendarMoodResponseDto.java b/src/main/java/com/aisip/OnO/backend/learningcalendar/dto/LearningCalendarMoodResponseDto.java new file mode 100644 index 00000000..9e6b1c41 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/learningcalendar/dto/LearningCalendarMoodResponseDto.java @@ -0,0 +1,9 @@ +package com.aisip.OnO.backend.learningcalendar.dto; + +import java.time.LocalDate; + +public record LearningCalendarMoodResponseDto( + LocalDate date, + String emojiKey +) { +} diff --git a/src/main/java/com/aisip/OnO/backend/learningcalendar/dto/LearningCalendarResponseDto.java b/src/main/java/com/aisip/OnO/backend/learningcalendar/dto/LearningCalendarResponseDto.java index 3ec4d35d..9d965435 100644 --- a/src/main/java/com/aisip/OnO/backend/learningcalendar/dto/LearningCalendarResponseDto.java +++ b/src/main/java/com/aisip/OnO/backend/learningcalendar/dto/LearningCalendarResponseDto.java @@ -22,7 +22,8 @@ public record DailyStudyRecord( int reviewCount, int noteWriteCount, int studyMinutes, - List reviewedItems + List reviewedItems, + String moodEmojiKey ) { } } diff --git a/src/main/java/com/aisip/OnO/backend/learningcalendar/entity/LearningCalendarMood.java b/src/main/java/com/aisip/OnO/backend/learningcalendar/entity/LearningCalendarMood.java new file mode 100644 index 00000000..41b611ca --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/learningcalendar/entity/LearningCalendarMood.java @@ -0,0 +1,52 @@ +package com.aisip.OnO.backend.learningcalendar.entity; + +import com.aisip.OnO.backend.common.entity.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Entity +@Getter +@Builder(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "learning_calendar_mood", + uniqueConstraints = @UniqueConstraint(name = "uk_learning_calendar_mood_user_date", columnNames = {"user_id", "study_date"})) +public class LearningCalendarMood extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "study_date", nullable = false) + private LocalDate studyDate; + + @Column(name = "emoji_key", nullable = false, length = 80) + private String emojiKey; + + public static LearningCalendarMood create(Long userId, LocalDate studyDate, String emojiKey) { + return LearningCalendarMood.builder() + .userId(userId) + .studyDate(studyDate) + .emojiKey(emojiKey) + .build(); + } + + public void updateEmojiKey(String emojiKey) { + this.emojiKey = emojiKey; + } +} diff --git a/src/main/java/com/aisip/OnO/backend/learningcalendar/exception/LearningCalendarErrorCase.java b/src/main/java/com/aisip/OnO/backend/learningcalendar/exception/LearningCalendarErrorCase.java new file mode 100644 index 00000000..e7e142c4 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/learningcalendar/exception/LearningCalendarErrorCase.java @@ -0,0 +1,17 @@ +package com.aisip.OnO.backend.learningcalendar.exception; + +import com.aisip.OnO.backend.common.exception.ErrorCase; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum LearningCalendarErrorCase implements ErrorCase { + + CALENDAR_RECORD_NOT_FOUND(404, 12001, "해당 날짜 학습 기록이 없습니다."), + INVALID_DATE_FORMAT(400, 12002, "날짜 형식이 올바르지 않습니다."); + + private final Integer httpStatusCode; + private final Integer errorCode; + private final String message; +} diff --git a/src/main/java/com/aisip/OnO/backend/learningcalendar/repository/LearningCalendarMoodRepository.java b/src/main/java/com/aisip/OnO/backend/learningcalendar/repository/LearningCalendarMoodRepository.java new file mode 100644 index 00000000..2295e6bf --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/learningcalendar/repository/LearningCalendarMoodRepository.java @@ -0,0 +1,15 @@ +package com.aisip.OnO.backend.learningcalendar.repository; + +import com.aisip.OnO.backend.learningcalendar.entity.LearningCalendarMood; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +public interface LearningCalendarMoodRepository extends JpaRepository { + + Optional findByUserIdAndStudyDate(Long userId, LocalDate studyDate); + + List findAllByUserIdAndStudyDateBetween(Long userId, LocalDate startDate, LocalDate endDate); +} diff --git a/src/main/java/com/aisip/OnO/backend/learningcalendar/repository/LearningCalendarQueryRepository.java b/src/main/java/com/aisip/OnO/backend/learningcalendar/repository/LearningCalendarQueryRepository.java index 757ba4f2..7ef7b47f 100644 --- a/src/main/java/com/aisip/OnO/backend/learningcalendar/repository/LearningCalendarQueryRepository.java +++ b/src/main/java/com/aisip/OnO/backend/learningcalendar/repository/LearningCalendarQueryRepository.java @@ -128,6 +128,27 @@ public List findDistinctNoteWriteDatesTotal(Long userId) { .toList(); } + public boolean existsStudyRecord(Long userId, LocalDate date) { + LocalDateTime start = date.atStartOfDay(); + LocalDateTime end = date.atTime(23, 59, 59); + Integer reviewExists = queryFactory + .selectOne() + .from(problemSolve) + .where(problemSolve.userId.eq(userId) + .and(problemSolve.practicedAt.between(start, end))) + .fetchFirst(); + if (reviewExists != null) { + return true; + } + Integer noteWriteExists = queryFactory + .selectOne() + .from(problem) + .where(problem.userId.eq(userId) + .and(problem.createdAt.between(start, end))) + .fetchFirst(); + return noteWriteExists != null; + } + private int secondsToMinutes(Integer seconds) { return seconds == null ? 0 : seconds / 60; } diff --git a/src/main/java/com/aisip/OnO/backend/learningcalendar/service/LearningCalendarService.java b/src/main/java/com/aisip/OnO/backend/learningcalendar/service/LearningCalendarService.java index 7b304ed1..9b4ceaa1 100644 --- a/src/main/java/com/aisip/OnO/backend/learningcalendar/service/LearningCalendarService.java +++ b/src/main/java/com/aisip/OnO/backend/learningcalendar/service/LearningCalendarService.java @@ -1,6 +1,13 @@ package com.aisip.OnO.backend.learningcalendar.service; +import com.aisip.OnO.backend.common.emoji.CustomEmojiValidator; +import com.aisip.OnO.backend.common.exception.ApplicationException; +import com.aisip.OnO.backend.learningcalendar.dto.LearningCalendarMoodRequestDto; +import com.aisip.OnO.backend.learningcalendar.dto.LearningCalendarMoodResponseDto; import com.aisip.OnO.backend.learningcalendar.dto.LearningCalendarResponseDto; +import com.aisip.OnO.backend.learningcalendar.entity.LearningCalendarMood; +import com.aisip.OnO.backend.learningcalendar.exception.LearningCalendarErrorCase; +import com.aisip.OnO.backend.learningcalendar.repository.LearningCalendarMoodRepository; import com.aisip.OnO.backend.learningcalendar.repository.LearningCalendarQueryRepository; import com.aisip.OnO.backend.util.redis.StreakCacheService; import lombok.RequiredArgsConstructor; @@ -24,6 +31,8 @@ public class LearningCalendarService { private final LearningCalendarQueryRepository calendarRepository; private final StreakCacheService streakCacheService; + private final LearningCalendarMoodRepository moodRepository; + private final CustomEmojiValidator customEmojiValidator; public LearningCalendarResponseDto getLearningCalendar(Long userId, int year, int month) { return getLearningCalendar(userId, year, month, LocalDate.now()); @@ -45,13 +54,17 @@ LearningCalendarResponseDto getLearningCalendar(Long userId, int year, int month Map> reviewedItems = toReviewedItemsMap( calendarRepository.findReviewItems(userId, start, end) ); + Map moodEmojiKeys = moodRepository.findAllByUserIdAndStudyDateBetween(userId, startDate, endDate) + .stream() + .collect(Collectors.toMap(LearningCalendarMood::getStudyDate, LearningCalendarMood::getEmojiKey)); List records = startDate.datesUntil(endDate.plusDays(1)) .map(date -> buildDailyRecord( date, reviewStats.get(date), noteWriteStats.get(date), - reviewedItems.getOrDefault(date, List.of()) + reviewedItems.getOrDefault(date, List.of()), + moodEmojiKeys.get(date) )) .toList(); @@ -71,11 +84,27 @@ LearningCalendarResponseDto getLearningCalendar(Long userId, int year, int month .build(); } + @Transactional + public LearningCalendarMoodResponseDto updateMood(Long userId, LearningCalendarMoodRequestDto request) { + if (request == null || request.date() == null) { + throw new ApplicationException(LearningCalendarErrorCase.INVALID_DATE_FORMAT); + } + customEmojiValidator.validate(request.emojiKey()); + if (!calendarRepository.existsStudyRecord(userId, request.date())) { + throw new ApplicationException(LearningCalendarErrorCase.CALENDAR_RECORD_NOT_FOUND); + } + LearningCalendarMood mood = moodRepository.findByUserIdAndStudyDate(userId, request.date()) + .orElseGet(() -> moodRepository.save(LearningCalendarMood.create(userId, request.date(), request.emojiKey()))); + mood.updateEmojiKey(request.emojiKey()); + return new LearningCalendarMoodResponseDto(mood.getStudyDate(), mood.getEmojiKey()); + } + private LearningCalendarResponseDto.DailyStudyRecord buildDailyRecord( LocalDate date, LearningCalendarQueryRepository.DailyReviewStat reviewStat, LearningCalendarQueryRepository.DailyNoteWriteStat noteWriteStat, - List reviewedItems + List reviewedItems, + String moodEmojiKey ) { int reviewCount = reviewStat == null ? 0 : toInt(reviewStat.reviewCount()); int noteWriteCount = noteWriteStat == null ? 0 : toInt(noteWriteStat.noteWriteCount()); @@ -88,6 +117,7 @@ private LearningCalendarResponseDto.DailyStudyRecord buildDailyRecord( .noteWriteCount(noteWriteCount) .studyMinutes(studyMinutes) .reviewedItems(reviewedItems) + .moodEmojiKey(moodEmojiKey) .build(); } diff --git a/src/test/java/com/aisip/OnO/backend/learningcalendar/controller/LearningCalendarControllerTest.java b/src/test/java/com/aisip/OnO/backend/learningcalendar/controller/LearningCalendarControllerTest.java index bb06ec18..c1aeb064 100644 --- a/src/test/java/com/aisip/OnO/backend/learningcalendar/controller/LearningCalendarControllerTest.java +++ b/src/test/java/com/aisip/OnO/backend/learningcalendar/controller/LearningCalendarControllerTest.java @@ -20,6 +20,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -53,6 +54,7 @@ void getLearningCalendar() throws Exception { .noteWriteCount(1) .studyMinutes(12) .reviewedItems(List.of("이차방정식 오답노트")) + .moodEmojiKey("happy_tears") .build())) .build(); given(learningCalendarService.getLearningCalendar(7L, 2026, 5)).willReturn(response); @@ -72,11 +74,37 @@ void getLearningCalendar() throws Exception { .andExpect(jsonPath("$.data.records[0].reviewCount").value(2)) .andExpect(jsonPath("$.data.records[0].noteWriteCount").value(1)) .andExpect(jsonPath("$.data.records[0].studyMinutes").value(12)) - .andExpect(jsonPath("$.data.records[0].reviewedItems[0]").value("이차방정식 오답노트")); + .andExpect(jsonPath("$.data.records[0].reviewedItems[0]").value("이차방정식 오답노트")) + .andExpect(jsonPath("$.data.records[0].moodEmojiKey").value("happy_tears")); verify(learningCalendarService).getLearningCalendar(7L, 2026, 5); } + @Test + @DisplayName("학습 달력 감정 이모지 저장") + @WithMockCustomUser(userId = 7L) + void updateMood() throws Exception { + given(learningCalendarService.updateMood(7L, new com.aisip.OnO.backend.learningcalendar.dto.LearningCalendarMoodRequestDto( + LocalDate.of(2026, 6, 7), + "happy_tears" + ))).willReturn(new com.aisip.OnO.backend.learningcalendar.dto.LearningCalendarMoodResponseDto( + LocalDate.of(2026, 6, 7), + "happy_tears" + )); + + mockMvc.perform(patch("/api/learning-calendar/mood") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "date": "2026-06-07", + "emojiKey": "happy_tears" + } + """)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.date").value("2026-06-07")) + .andExpect(jsonPath("$.data.emojiKey").value("happy_tears")); + } + @Test @DisplayName("학습 달력 조회 - 잘못된 월은 400을 반환한다") @WithMockCustomUser(userId = 7L) diff --git a/src/test/java/com/aisip/OnO/backend/learningcalendar/service/LearningCalendarServiceTest.java b/src/test/java/com/aisip/OnO/backend/learningcalendar/service/LearningCalendarServiceTest.java index 9bf8d020..b546de73 100644 --- a/src/test/java/com/aisip/OnO/backend/learningcalendar/service/LearningCalendarServiceTest.java +++ b/src/test/java/com/aisip/OnO/backend/learningcalendar/service/LearningCalendarServiceTest.java @@ -1,6 +1,9 @@ package com.aisip.OnO.backend.learningcalendar.service; +import com.aisip.OnO.backend.common.exception.ApplicationException; +import com.aisip.OnO.backend.learningcalendar.dto.LearningCalendarMoodRequestDto; import com.aisip.OnO.backend.learningcalendar.dto.LearningCalendarResponseDto; +import com.aisip.OnO.backend.learningcalendar.exception.LearningCalendarErrorCase; import com.aisip.OnO.backend.problem.dto.ProblemRegisterDto; import com.aisip.OnO.backend.problem.entity.Problem; import com.aisip.OnO.backend.problem.repository.ProblemRepository; @@ -24,6 +27,7 @@ import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; @SpringBootTest @ActiveProfiles("test") @@ -96,6 +100,42 @@ void getLearningCalendar_success() { assertThat(records.get(LocalDate.of(2026, 5, 2)).noteWriteCount()).isEqualTo(1); assertThat(records.get(LocalDate.of(2026, 5, 6)).hasStudied()).isFalse(); assertThat(records.get(LocalDate.of(2026, 5, 6)).reviewedItems()).isEmpty(); + assertThat(records.get(LocalDate.of(2026, 5, 1)).moodEmojiKey()).isNull(); + } + + @Test + @DisplayName("학습 기록이 있는 날짜에 감정 이모지를 저장하고 조회한다") + void updateMood_success() { + User targetUser = createUser("calendar-mood-user"); + Long userId = targetUser.getId(); + createProblemWrite(userId, LocalDateTime.of(2026, 6, 7, 8, 0)); + + learningCalendarService.updateMood(userId, + new LearningCalendarMoodRequestDto(LocalDate.of(2026, 6, 7), "happy_tears")); + learningCalendarService.updateMood(userId, + new LearningCalendarMoodRequestDto(LocalDate.of(2026, 6, 7), "success_checkmark")); + + LearningCalendarResponseDto response = learningCalendarService.getLearningCalendar( + userId, + 2026, + 6, + LocalDate.of(2026, 6, 8) + ); + + Map records = response.records().stream() + .collect(Collectors.toMap(LearningCalendarResponseDto.DailyStudyRecord::date, record -> record)); + assertThat(records.get(LocalDate.of(2026, 6, 7)).moodEmojiKey()).isEqualTo("success_checkmark"); + } + + @Test + @DisplayName("학습 기록이 없는 날짜에는 감정 이모지를 저장할 수 없다") + void updateMood_recordNotFound() { + User targetUser = createUser("calendar-mood-empty-user"); + + assertThatThrownBy(() -> learningCalendarService.updateMood(targetUser.getId(), + new LearningCalendarMoodRequestDto(LocalDate.of(2026, 6, 7), "happy_tears"))) + .isInstanceOf(ApplicationException.class) + .hasMessageContaining(LearningCalendarErrorCase.CALENDAR_RECORD_NOT_FOUND.getMessage()); } @Test From b5897a848550cf6c277cfdc4f5938c3a18059094 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sun, 7 Jun 2026 14:18:46 +0900 Subject: [PATCH 22/67] =?UTF-8?q?[Feat]=20=EB=B3=B5=EC=8A=B5=EB=85=B8?= =?UTF-8?q?=ED=8A=B8=20=EC=99=84=EB=A3=8C=20=EC=8B=9C=20=EC=86=8C=EA=B0=90?= =?UTF-8?q?=20=EC=9D=B4=EB=AA=A8=EC=A7=80=20=EC=A0=80=EC=9E=A5=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80=20-=20=EB=B3=B5=EC=8A=B5=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=EC=9A=94=EC=B2=AD=EC=97=90=20moodEmojiKey?= =?UTF-8?q?=20=EC=98=B5=EC=85=98=20=EC=B6=94=EA=B0=80,=20=EC=B5=9C?= =?UTF-8?q?=EC=8B=A0=20=EB=B3=B5=EC=8A=B5=EB=85=B8=ED=8A=B8=EC=97=90=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EB=B0=8F=20=EC=9D=91=EB=8B=B5=20=EB=85=B8?= =?UTF-8?q?=EC=B6=9C=20-=20=ED=97=88=EC=9A=A9=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EC=9D=80=20=EC=9D=B4=EB=AA=A8=EC=A7=80=20=ED=82=A4=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PracticeNoteController.java | 5 +-- .../dto/PracticeNoteCompleteRequestDto.java | 6 ++++ .../dto/PracticeNoteDetailResponseDto.java | 3 ++ .../dto/PracticeNoteThumbnailResponseDto.java | 5 ++- .../practicenote/entity/PracticeNote.java | 8 +++++ .../service/PracticeNoteService.java | 11 ++++++- .../service/PracticeNoteServiceTest.java | 31 +++++++++++++++++++ 7 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/aisip/OnO/backend/practicenote/dto/PracticeNoteCompleteRequestDto.java diff --git a/src/main/java/com/aisip/OnO/backend/practicenote/controller/PracticeNoteController.java b/src/main/java/com/aisip/OnO/backend/practicenote/controller/PracticeNoteController.java index 8f0027a8..02b9dc00 100644 --- a/src/main/java/com/aisip/OnO/backend/practicenote/controller/PracticeNoteController.java +++ b/src/main/java/com/aisip/OnO/backend/practicenote/controller/PracticeNoteController.java @@ -73,9 +73,10 @@ public CommonResponse registerPractice(@RequestBody PracticeNoteRegisterDt // ✅ 복습 완료 횟수 증가 @PatchMapping("/{practiceId}/complete") - public CommonResponse addPracticeCount(@PathVariable("practiceId") Long practiceId) { + public CommonResponse addPracticeCount(@PathVariable("practiceId") Long practiceId, + @RequestBody(required = false) PracticeNoteCompleteRequestDto request) { Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - practiceNoteService.addPracticeNoteCount(userId, practiceId); + practiceNoteService.addPracticeNoteCount(userId, practiceId, request); log.info("userId: {} completed problem practice with practiceNoteId: {}", userId, practiceId); return CommonResponse.success("복습을 완료했습니다."); diff --git a/src/main/java/com/aisip/OnO/backend/practicenote/dto/PracticeNoteCompleteRequestDto.java b/src/main/java/com/aisip/OnO/backend/practicenote/dto/PracticeNoteCompleteRequestDto.java new file mode 100644 index 00000000..9b174d52 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/practicenote/dto/PracticeNoteCompleteRequestDto.java @@ -0,0 +1,6 @@ +package com.aisip.OnO.backend.practicenote.dto; + +public record PracticeNoteCompleteRequestDto( + String moodEmojiKey +) { +} diff --git a/src/main/java/com/aisip/OnO/backend/practicenote/dto/PracticeNoteDetailResponseDto.java b/src/main/java/com/aisip/OnO/backend/practicenote/dto/PracticeNoteDetailResponseDto.java index a6863b7a..e434a949 100644 --- a/src/main/java/com/aisip/OnO/backend/practicenote/dto/PracticeNoteDetailResponseDto.java +++ b/src/main/java/com/aisip/OnO/backend/practicenote/dto/PracticeNoteDetailResponseDto.java @@ -21,6 +21,8 @@ public record PracticeNoteDetailResponseDto( LocalDateTime lastSolvedAt, + String lastSessionMoodEmojiKey, + LocalDateTime createdAt, LocalDateTime updatedAt @@ -41,6 +43,7 @@ public static PracticeNoteDetailResponseDto from(@NotNull PracticeNote practiceN .problemIdList(problemIdList) .practiceNotification(notificationDto) .lastSolvedAt(practiceNote.getLastSolvedAt()) + .lastSessionMoodEmojiKey(practiceNote.getLastSessionMoodEmojiKey()) .createdAt(practiceNote.getCreatedAt()) .updatedAt(practiceNote.getUpdatedAt()) .build(); diff --git a/src/main/java/com/aisip/OnO/backend/practicenote/dto/PracticeNoteThumbnailResponseDto.java b/src/main/java/com/aisip/OnO/backend/practicenote/dto/PracticeNoteThumbnailResponseDto.java index a8f09af0..da64cef7 100644 --- a/src/main/java/com/aisip/OnO/backend/practicenote/dto/PracticeNoteThumbnailResponseDto.java +++ b/src/main/java/com/aisip/OnO/backend/practicenote/dto/PracticeNoteThumbnailResponseDto.java @@ -15,7 +15,9 @@ public record PracticeNoteThumbnailResponseDto( Long practiceCount, - LocalDateTime lastSolvedAt + LocalDateTime lastSolvedAt, + + String lastSessionMoodEmojiKey ) { public static PracticeNoteThumbnailResponseDto from(@NotNull PracticeNote practiceNote) { return PracticeNoteThumbnailResponseDto.builder() @@ -23,6 +25,7 @@ public static PracticeNoteThumbnailResponseDto from(@NotNull PracticeNote practi .practiceTitle(practiceNote.getTitle()) .practiceCount(practiceNote.getPracticeCount()) .lastSolvedAt(practiceNote.getLastSolvedAt()) + .lastSessionMoodEmojiKey(practiceNote.getLastSessionMoodEmojiKey()) .build(); } } diff --git a/src/main/java/com/aisip/OnO/backend/practicenote/entity/PracticeNote.java b/src/main/java/com/aisip/OnO/backend/practicenote/entity/PracticeNote.java index 1d123131..d8419312 100644 --- a/src/main/java/com/aisip/OnO/backend/practicenote/entity/PracticeNote.java +++ b/src/main/java/com/aisip/OnO/backend/practicenote/entity/PracticeNote.java @@ -35,6 +35,9 @@ public class PracticeNote extends BaseEntity { private LocalDateTime lastSolvedAt; + @Column(length = 80) + private String lastSessionMoodEmojiKey; + @Embedded private PracticeNotification practiceNotification; @@ -63,6 +66,11 @@ public void updatePracticeNoteCount() { this.lastSolvedAt = LocalDateTime.now(); } + public void updatePracticeNoteCount(String moodEmojiKey) { + updatePracticeNoteCount(); + this.lastSessionMoodEmojiKey = moodEmojiKey; + } + public void updateNotification(PracticeNotification practiceNotification) { this.practiceNotification = practiceNotification; } diff --git a/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java b/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java index a84f5abf..666657f5 100644 --- a/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java +++ b/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java @@ -1,6 +1,7 @@ package com.aisip.OnO.backend.practicenote.service; import com.aisip.OnO.backend.admin.dto.AdminPracticeNoteResponseDto; +import com.aisip.OnO.backend.common.emoji.CustomEmojiValidator; import com.aisip.OnO.backend.common.exception.ApplicationException; import com.aisip.OnO.backend.common.response.CursorPageResponse; import com.aisip.OnO.backend.mission.service.MissionLogService; @@ -57,6 +58,8 @@ public class PracticeNoteService { private final UserRepository userRepository; + private final CustomEmojiValidator customEmojiValidator; + private PracticeNote getPracticeEntity(Long practiceId, Long userId){ PracticeNote practiceNote = practiceNoteRepository.findById(practiceId) @@ -137,8 +140,14 @@ public List findAllPracticesByUser(Long userId){ } public void addPracticeNoteCount(Long userId, Long practiceId) { + addPracticeNoteCount(userId, practiceId, null); + } + + public void addPracticeNoteCount(Long userId, Long practiceId, PracticeNoteCompleteRequestDto request) { PracticeNote practiceNote = getPracticeEntity(practiceId, userId); - practiceNote.updatePracticeNoteCount(); + String moodEmojiKey = request == null ? null : request.moodEmojiKey(); + customEmojiValidator.validateNullable(moodEmojiKey); + practiceNote.updatePracticeNoteCount(moodEmojiKey); // 복습노트 사용 미션 등록 missionLogService.registerNotePracticeMission(userId, practiceId); diff --git a/src/test/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteServiceTest.java b/src/test/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteServiceTest.java index d704bdfa..415be5f0 100644 --- a/src/test/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteServiceTest.java +++ b/src/test/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteServiceTest.java @@ -1,10 +1,12 @@ package com.aisip.OnO.backend.practicenote.service; import com.aisip.OnO.backend.common.exception.ApplicationException; +import com.aisip.OnO.backend.common.emoji.CustomEmojiErrorCase; import com.aisip.OnO.backend.folder.dto.FolderRegisterDto; import com.aisip.OnO.backend.folder.entity.Folder; import com.aisip.OnO.backend.folder.repository.FolderRepository; import com.aisip.OnO.backend.practicenote.dto.PracticeNoteDetailResponseDto; +import com.aisip.OnO.backend.practicenote.dto.PracticeNoteCompleteRequestDto; import com.aisip.OnO.backend.practicenote.dto.PracticeNoteRegisterDto; import com.aisip.OnO.backend.practicenote.dto.PracticeNoteThumbnailResponseDto; import com.aisip.OnO.backend.practicenote.dto.PracticeNoteUpdateDto; @@ -282,6 +284,35 @@ void addPracticeNoteCount() { assertThat(practiceNote.getPracticeCount()).isEqualTo(1); } + @Test + @DisplayName("복습 완료 소감 이모지를 최신 복습노트에 저장한다") + void addPracticeNoteCount_withMoodEmojiKey() { + Long practiceNoteId = practiceNoteList.get(0).getId(); + + practiceNoteService.addPracticeNoteCount(userId, practiceNoteId, + new PracticeNoteCompleteRequestDto("success_checkmark")); + + PracticeNote practiceNote = practiceNoteRepository.findById(practiceNoteId).get(); + assertThat(practiceNote.getPracticeCount()).isEqualTo(1); + assertThat(practiceNote.getLastSessionMoodEmojiKey()).isEqualTo("success_checkmark"); + + PracticeNoteDetailResponseDto detail = practiceNoteService.findPracticeNoteDetail(practiceNoteId, userId); + assertThat(detail.lastSessionMoodEmojiKey()).isEqualTo("success_checkmark"); + assertThat(practiceNoteService.findAllPracticeThumbnailsByUser(userId).get(0).lastSessionMoodEmojiKey()) + .isEqualTo("success_checkmark"); + } + + @Test + @DisplayName("지원하지 않는 복습 완료 소감 이모지는 예외") + void addPracticeNoteCount_invalidMoodEmojiKey() { + Long practiceNoteId = practiceNoteList.get(0).getId(); + + assertThatThrownBy(() -> practiceNoteService.addPracticeNoteCount(userId, practiceNoteId, + new PracticeNoteCompleteRequestDto("not_supported"))) + .isInstanceOf(ApplicationException.class) + .hasMessageContaining(CustomEmojiErrorCase.INVALID_EMOJI_KEY.getMessage()); + } + @Test @DisplayName("복습 노트 수정 - 문제 리스트 수정") void updatePracticeInfo() { From 8a02937022c1363cfbd4346724a8202861b6bb71 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sun, 7 Jun 2026 14:19:38 +0900 Subject: [PATCH 23/67] =?UTF-8?q?[Refactor]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20=EB=B0=98=EC=9D=91=20=EC=9D=B4=EB=AA=A8=EC=A7=80?= =?UTF-8?q?=EB=A5=BC=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=9D=B4=EB=AA=A8?= =?UTF-8?q?=EC=A7=80=20=EA=B2=80=EC=A6=9D=EC=9C=BC=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=20-=20=ED=94=BC=EB=93=9C/=EA=B3=B5=EC=9C=A0=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EB=B0=98=EC=9D=91=EC=9D=98=20=EA=B3=A0=EC=A0=95=20?= =?UTF-8?q?=EC=9D=B4=EB=AA=A8=EC=A7=80=20=ED=99=94=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EA=B3=B5=ED=86=B5=20Cus?= =?UTF-8?q?tomEmojiValidator=EB=A1=9C=20=EB=8C=80=EC=B2=B4=20-=20=EB=B0=98?= =?UTF-8?q?=EC=9D=91=20emoji=20=EC=BB=AC=EB=9F=BC=20=EA=B8=B8=EC=9D=B4?= =?UTF-8?q?=EB=A5=BC=2080=EC=9E=90=EB=A1=9C=20=ED=99=95=EC=9E=A5=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98(V9)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/studyroom/entity/StudyRoomFeedReaction.java | 2 +- .../studyroom/entity/StudyRoomSharedProblemReaction.java | 2 +- .../backend/studyroom/service/StudyRoomFeedService.java | 7 +++---- .../studyroom/service/StudyRoomSharedProblemService.java | 9 +++------ .../db/migration/V9__widen_study_room_reaction_emoji.sql | 5 +++++ 5 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 src/main/resources/db/migration/V9__widen_study_room_reaction_emoji.sql diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeedReaction.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeedReaction.java index 6ac32267..aac6c897 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeedReaction.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeedReaction.java @@ -26,7 +26,7 @@ public class StudyRoomFeedReaction extends BaseEntity { @JoinColumn(name = "user_id", nullable = false) private User user; - @Column(nullable = false, length = 16) + @Column(nullable = false, length = 80) private String emoji; public static StudyRoomFeedReaction create(StudyRoomFeed feed, User user, String emoji) { diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblemReaction.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblemReaction.java index b6f08fab..bbd2fac5 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblemReaction.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblemReaction.java @@ -26,7 +26,7 @@ public class StudyRoomSharedProblemReaction extends BaseEntity { @JoinColumn(name = "user_id", nullable = false) private User user; - @Column(nullable = false, length = 16) + @Column(nullable = false, length = 80) private String emoji; public static StudyRoomSharedProblemReaction create(StudyRoomSharedProblem sharedProblem, User user, String emoji) { diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedService.java index 518c983f..71c93cbf 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedService.java @@ -1,5 +1,6 @@ package com.aisip.OnO.backend.studyroom.service; +import com.aisip.OnO.backend.common.emoji.CustomEmojiValidator; import com.aisip.OnO.backend.common.exception.ApplicationException; import com.aisip.OnO.backend.common.response.CursorPageResponse; import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; @@ -35,7 +36,6 @@ public class StudyRoomFeedService { private static final ZoneId KST = ZoneId.of("Asia/Seoul"); - private static final Set ALLOWED_FEED_EMOJIS = Set.of("🔥", "👍", "🎉"); private static final TypeReference> MAP_TYPE = new TypeReference<>() { }; @@ -46,6 +46,7 @@ public class StudyRoomFeedService { private final UserRepository userRepository; private final ObjectMapper objectMapper; private final StudyRoomReactionService reactionService; + private final CustomEmojiValidator customEmojiValidator; @Transactional(readOnly = true) public CursorPageResponse getFeed(Long roomId, Long userId, Long cursor, int size) { @@ -68,9 +69,7 @@ public CursorPageResponse getFeed(Long roomId, Long userId, Lo @Transactional public FeedReactionToggleResponse toggleReaction(Long roomId, Long feedId, Long userId, ReactionToggleRequest request) { accessService.validateMember(roomId, userId); - if (!ALLOWED_FEED_EMOJIS.contains(request.emoji())) { - throw new ApplicationException(StudyRoomErrorCase.INVALID_REACTION_EMOJI); - } + customEmojiValidator.validate(request.emoji()); StudyRoomFeed feed = feedRepository.findByIdAndRoomId(feedId, roomId) .orElseThrow(() -> new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_NOT_FOUND)); User user = userRepository.findById(userId) diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java index c264161f..dea70a29 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java @@ -1,5 +1,6 @@ package com.aisip.OnO.backend.studyroom.service; +import com.aisip.OnO.backend.common.emoji.CustomEmojiValidator; import com.aisip.OnO.backend.common.exception.ApplicationException; import com.aisip.OnO.backend.common.response.CursorPageResponse; import com.aisip.OnO.backend.problem.entity.Problem; @@ -22,15 +23,12 @@ import java.util.List; import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class StudyRoomSharedProblemService { - private static final Set ALLOWED_SHARED_PROBLEM_EMOJIS = Set.of("😱", "🔥", "👍", "🎉"); - private final StudyRoomAccessService accessService; private final StudyRoomSharedProblemRepository sharedProblemRepository; private final StudyRoomSharedProblemReactionRepository reactionRepository; @@ -38,6 +36,7 @@ public class StudyRoomSharedProblemService { private final ProblemService problemService; private final StudyRoomReactionService reactionService; private final ApplicationEventPublisher eventPublisher; + private final CustomEmojiValidator customEmojiValidator; @Transactional(readOnly = true) public CursorPageResponse getSharedProblems(Long roomId, Long userId, Long cursor, int size) { @@ -79,9 +78,7 @@ public SharedProblemResponse shareProblem(Long roomId, Long userId, SharedProble @Transactional public SharedProblemReactionToggleResponse toggleReaction(Long roomId, Long sharedProblemId, Long userId, ReactionToggleRequest request) { accessService.validateMember(roomId, userId); - if (!ALLOWED_SHARED_PROBLEM_EMOJIS.contains(request.emoji())) { - throw new ApplicationException(StudyRoomErrorCase.INVALID_REACTION_EMOJI); - } + customEmojiValidator.validate(request.emoji()); StudyRoomSharedProblem sharedProblem = sharedProblemRepository.findByIdAndRoomId(sharedProblemId, roomId) .orElseThrow(() -> new ApplicationException(StudyRoomErrorCase.SHARED_PROBLEM_NOT_FOUND)); User user = userRepository.findById(userId) diff --git a/src/main/resources/db/migration/V9__widen_study_room_reaction_emoji.sql b/src/main/resources/db/migration/V9__widen_study_room_reaction_emoji.sql new file mode 100644 index 00000000..0be514a0 --- /dev/null +++ b/src/main/resources/db/migration/V9__widen_study_room_reaction_emoji.sql @@ -0,0 +1,5 @@ +ALTER TABLE study_room_feed_reaction + MODIFY COLUMN emoji VARCHAR(80) NOT NULL; + +ALTER TABLE study_room_shared_problem_reaction + MODIFY COLUMN emoji VARCHAR(80) NOT NULL; \ No newline at end of file From ad88a617d016f60a5719c72d4a8b3c4f186ebb04 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sun, 7 Jun 2026 14:20:15 +0900 Subject: [PATCH 24/67] =?UTF-8?q?[Feat]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20=EC=B1=8C=EB=A6=B0=EC=A7=80=20=EC=B8=A1=EC=A0=95=20?= =?UTF-8?q?=EC=A7=80=ED=91=9C=20=ED=99=95=EC=9E=A5=20=EB=B0=8F=20=EC=98=A4?= =?UTF-8?q?=EB=8A=98=EC=9D=98=20=EB=B3=B5=EC=8A=B5=20=ED=98=84=ED=99=A9=20?= =?UTF-8?q?=ED=86=B5=EA=B3=84=20=EC=B6=94=EA=B0=80=20-=20ATTENDANCE=20?= =?UTF-8?q?=EB=93=B1=20=EC=8B=A0=EA=B7=9C=20=EC=B8=A1=EC=A0=95=20=EC=A7=80?= =?UTF-8?q?=ED=91=9C=EC=99=80=20=EC=B1=8C=EB=A6=B0=EC=A7=80=20=EA=B8=B0?= =?UTF-8?q?=EA=B0=84(DAILY/WEEKLY/MONTHLY)=20=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80(V8)=20-=20=EA=B8=B0=EC=A1=B4=20WEEKLY=5FPROB?= =?UTF-8?q?LEM=5FCOUNT=20=EB=93=B1=EC=9D=80=20=EC=97=AD=EC=A7=81=EB=A0=AC?= =?UTF-8?q?=ED=99=94=20=ED=98=B8=ED=99=98=EC=9D=84=20=EC=9C=84=ED=95=B4=20?= =?UTF-8?q?@Deprecated=20=EC=9C=A0=EC=A7=80,=20=EC=B1=8C=EB=A6=B0=EC=A7=80?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20=EA=B6=8C=ED=95=9C=EC=9D=84=20=EB=B0=A9?= =?UTF-8?q?=EC=9E=A5=EC=97=90=EC=84=9C=20=EB=A9=A4=EB=B2=84=EB=A1=9C=20?= =?UTF-8?q?=EC=99=84=ED=99=94=20-=20=EB=B0=A9=20=EB=AA=A9=EB=A1=9D/?= =?UTF-8?q?=EC=83=81=EC=84=B8=EC=97=90=20=EB=A9=A4=EB=B2=84=EB=B3=84=C2=B7?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EC=98=A4=EB=8A=98=20=EB=B3=B5=EC=8A=B5=20?= =?UTF-8?q?=ED=9A=9F=EC=88=98=C2=B7=EC=9D=B8=EC=9B=90=20=ED=86=B5=EA=B3=84?= =?UTF-8?q?=20=EB=85=B8=EC=B6=9C,=20=EB=A9=A4=EB=B2=84=20=EC=88=98=20?= =?UTF-8?q?=EC=9D=BC=EA=B4=84=20=EC=A1=B0=ED=9A=8C=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20=ED=86=B5=ED=95=A9=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=9D=98=20=EB=B0=98=EC=9D=91=20=EC=9D=B4?= =?UTF-8?q?=EB=AA=A8=EC=A7=80=20=ED=91=9C=EA=B8=B0=EB=A5=BC=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=EC=9D=B4=EB=AA=A8=EC=A7=80=20=ED=82=A4?= =?UTF-8?q?=EB=A1=9C=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/studyroom/dto/StudyRoomDtos.java | 14 ++-- .../dto/StudyRoomTodayPracticeSummary.java | 8 ++ .../studyroom/entity/StudyRoomChallenge.java | 9 +- .../entity/StudyRoomChallengeMetric.java | 8 ++ .../entity/StudyRoomChallengePeriod.java | 7 ++ .../repository/StudyRoomMemberRepository.java | 8 ++ .../service/StudyRoomChallengeService.java | 30 +++++-- .../studyroom/service/StudyRoomMapper.java | 24 ++++-- .../studyroom/service/StudyRoomService.java | 20 ++++- .../service/StudyRoomStatsService.java | 50 +++++++++++ .../service/StudyRoomWeeklyReportService.java | 28 +++++-- .../V8__add_study_room_challenge_period.sql | 2 + .../StudyRoomServiceIntegrationTest.java | 82 ++++++++++++++++++- 13 files changed, 254 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomTodayPracticeSummary.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengePeriod.java create mode 100644 src/main/resources/db/migration/V8__add_study_room_challenge_period.sql diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java index b727861b..7f5533d5 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java @@ -22,15 +22,16 @@ public record StudyRoomGoalUpdateRequest(Integer weeklyGoal) { public record ReactionToggleRequest(String emoji) { } - public record ChallengeCreateRequest(String title, String type, String metric, Integer targetValue, - LocalDateTime startAt, LocalDateTime endAt) { + public record ChallengeCreateRequest(String title, String type, String metric, String period, + Integer targetValue, LocalDateTime startAt, LocalDateTime endAt) { } public record SharedProblemCreateRequest(Long problemId, String comment) { } public record StudyRoomListResponse(Long roomId, String name, Long hostUserId, int memberCount, - String thumbnailUrl, boolean hasUnreadReport) { + String thumbnailUrl, boolean hasUnreadReport, + int todayPracticeMemberCount, int todayPracticeCount) { } public record StudyRoomDetailResponse(Long roomId, String name, Long hostUserId, String thumbnailUrl, @@ -39,7 +40,8 @@ public record StudyRoomDetailResponse(Long roomId, String name, Long hostUserId, public record StudyRoomMemberResponse(Long userId, String name, Long totalStudyLevel, int currentStreak, int weeklyProblemCount, int weeklyPracticeCount, - Integer weeklyGoal, Integer goalProgress) { + Integer weeklyGoal, Integer goalProgress, + int todayPracticeCount) { } public record InviteCodeResponse(String code, LocalDateTime expiredAt) { @@ -62,8 +64,8 @@ public record FeedItemResponse(Long feedId, Long userId, String userName, String public record FeedReactionToggleResponse(Long feedId, List reactions) { } - public record ChallengeResponse(Long challengeId, String title, String type, String metric, Integer targetValue, - LocalDateTime startAt, LocalDateTime endAt, String status, + public record ChallengeResponse(Long challengeId, String title, String type, String metric, String period, + Integer targetValue, LocalDateTime startAt, LocalDateTime endAt, String status, List memberProgress, Integer groupCurrent) { } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomTodayPracticeSummary.java b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomTodayPracticeSummary.java new file mode 100644 index 00000000..53697e3e --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomTodayPracticeSummary.java @@ -0,0 +1,8 @@ +package com.aisip.OnO.backend.studyroom.dto; + +public record StudyRoomTodayPracticeSummary(int todayPracticeMemberCount, int todayPracticeCount) { + + public static StudyRoomTodayPracticeSummary empty() { + return new StudyRoomTodayPracticeSummary(0, 0); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallenge.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallenge.java index ff610170..ead47f93 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallenge.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallenge.java @@ -35,6 +35,10 @@ public class StudyRoomChallenge extends BaseEntity { @Column(nullable = false, length = 40) private StudyRoomChallengeMetric metric; + @Enumerated(EnumType.STRING) + @Column(name = "period", length = 20) + private StudyRoomChallengePeriod period; + @Column(name = "target_value", nullable = false) private Integer targetValue; @@ -49,13 +53,14 @@ public class StudyRoomChallenge extends BaseEntity { private StudyRoomChallengeStatus status; public static StudyRoomChallenge create(StudyRoom room, String title, StudyRoomChallengeType type, - StudyRoomChallengeMetric metric, Integer targetValue, - LocalDateTime startAt, LocalDateTime endAt) { + StudyRoomChallengeMetric metric, StudyRoomChallengePeriod period, + Integer targetValue, LocalDateTime startAt, LocalDateTime endAt) { return StudyRoomChallenge.builder() .room(room) .title(title) .type(type) .metric(metric) + .period(period) .targetValue(targetValue) .startAt(startAt) .endAt(endAt) diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengeMetric.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengeMetric.java index 915db165..ae5431a7 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengeMetric.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengeMetric.java @@ -1,7 +1,15 @@ package com.aisip.OnO.backend.studyroom.entity; public enum StudyRoomChallengeMetric { + PROBLEM_COUNT, + PRACTICE_COUNT, + ATTENDANCE, + + // 기존에 저장된 챌린지 역직렬화 호환용 — 신규 생성 시에는 사용하지 않는다. + @Deprecated WEEKLY_PROBLEM_COUNT, + @Deprecated WEEKLY_PRACTICE_COUNT, + @Deprecated STREAK } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengePeriod.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengePeriod.java new file mode 100644 index 00000000..364aa8b2 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallengePeriod.java @@ -0,0 +1,7 @@ +package com.aisip.OnO.backend.studyroom.entity; + +public enum StudyRoomChallengePeriod { + DAILY, + WEEKLY, + MONTHLY +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomMemberRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomMemberRepository.java index 0b180e64..7ea849ea 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomMemberRepository.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomMemberRepository.java @@ -34,6 +34,14 @@ public interface StudyRoomMemberRepository extends JpaRepository findAllWithRoomByUserId(@Param("userId") Long userId); + @Query(""" + select m.room.id, count(m.id) + from StudyRoomMember m + where m.room.id in :roomIds + group by m.room.id + """) + List countMembersByRoomIds(@Param("roomIds") Collection roomIds); + @Modifying @Query("delete from StudyRoomMember m where m.room.id = :roomId and m.user.id = :userId") void deleteByRoomIdAndUserId(@Param("roomId") Long roomId, @Param("userId") Long userId); diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java index 95d6d592..45714533 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java @@ -39,19 +39,23 @@ public List getChallenges(Long roomId, Long userId) { @Transactional public ChallengeResponse createChallenge(Long roomId, Long userId, ChallengeCreateRequest request) { - accessService.validateHost(roomId, userId); + accessService.validateMember(roomId, userId); validateCreateRequest(request); refreshRoomChallengeStatuses(roomId); if (challengeRepository.countByRoomIdAndStatus(roomId, StudyRoomChallengeStatus.IN_PROGRESS) >= MAX_IN_PROGRESS_CHALLENGE_COUNT) { throw new ApplicationException(StudyRoomErrorCase.CHALLENGE_LIMIT_EXCEEDED); } LocalDateTime startAt = request.startAt() == null ? LocalDateTime.now() : request.startAt(); + StudyRoomChallengePeriod period = request.period() == null + ? null + : parseEnum(request.period(), StudyRoomChallengePeriod.class); StudyRoom room = accessService.getRoomOrThrow(roomId); StudyRoomChallenge challenge = challengeRepository.save(StudyRoomChallenge.create( room, request.title().trim(), parseEnum(request.type(), StudyRoomChallengeType.class), parseEnum(request.metric(), StudyRoomChallengeMetric.class), + period, request.targetValue(), startAt, request.endAt() @@ -78,17 +82,24 @@ private ChallengeResponse toResponse(StudyRoomChallenge challenge, List streaks = statsService.currentStreaks(userIds, streakBaseDate, challenge.getStartAt().toLocalDate()); Map stats = statsService.getStats(userIds, challenge.getStartAt(), challenge.getEndAt(), streaks); + Map attendanceDays = isAttendanceMetric(challenge.getMetric()) + ? statsService.attendanceDayCounts(userIds, challenge.getStartAt(), challenge.getEndAt()) + : Map.of(); List memberProgress = challenge.getType() == StudyRoomChallengeType.GROUP ? List.of() : members.stream() .map(member -> { - int current = metricValue(challenge.getMetric(), stats.get(member.getUser().getId())); + int current = metricValue(challenge.getMetric(), stats.get(member.getUser().getId()), + attendanceDays.getOrDefault(member.getUser().getId(), 0)); return new ChallengeMemberProgressResponse(member.getUser().getId(), member.getUser().getName(), current, current >= challenge.getTargetValue()); }) .toList(); Integer groupCurrent = challenge.getType() == StudyRoomChallengeType.GROUP - ? stats.values().stream().mapToInt(stat -> metricValue(challenge.getMetric(), stat)).sum() + ? userIds.stream() + .mapToInt(memberId -> metricValue(challenge.getMetric(), stats.get(memberId), + attendanceDays.getOrDefault(memberId, 0))) + .sum() : null; StudyRoomChallengeStatus status = effectiveStatus(challenge, memberProgress, groupCurrent); if (persistStatus && challenge.getStatus() != status) { @@ -99,6 +110,7 @@ private ChallengeResponse toResponse(StudyRoomChallenge challenge, List stats.weeklyProblemCount(); - case WEEKLY_PRACTICE_COUNT -> stats.weeklyPracticeCount(); - case STREAK -> stats.currentStreak(); + case PROBLEM_COUNT, WEEKLY_PROBLEM_COUNT -> stats.weeklyProblemCount(); + case PRACTICE_COUNT, WEEKLY_PRACTICE_COUNT -> stats.weeklyPracticeCount(); + case ATTENDANCE, STREAK -> attendanceDays; }; } + private boolean isAttendanceMetric(StudyRoomChallengeMetric metric) { + return metric == StudyRoomChallengeMetric.ATTENDANCE || metric == StudyRoomChallengeMetric.STREAK; + } + private void validateCreateRequest(ChallengeCreateRequest request) { if (request.title() == null || request.title().isBlank() || request.title().length() > 40 || request.targetValue() == null || request.targetValue() < 1 diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomMapper.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomMapper.java index d1609923..2852236b 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomMapper.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomMapper.java @@ -3,6 +3,7 @@ import com.aisip.OnO.backend.mission.entity.UserMissionStatus; import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; import com.aisip.OnO.backend.studyroom.dto.StudyRoomStats; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomTodayPracticeSummary; import com.aisip.OnO.backend.studyroom.entity.StudyRoom; import com.aisip.OnO.backend.studyroom.entity.StudyRoomMember; import com.aisip.OnO.backend.studyroom.repository.StudyRoomWeeklyReportReadRepository; @@ -18,22 +19,30 @@ public class StudyRoomMapper { private final StudyRoomWeeklyReportReadRepository reportReadRepository; - public StudyRoomListResponse toListResponse(StudyRoomMember member) { + public StudyRoomListResponse toListResponse(StudyRoomMember member, int memberCount, + StudyRoomTodayPracticeSummary todayPracticeSummary) { StudyRoom room = member.getRoom(); return new StudyRoomListResponse( room.getId(), room.getName(), room.getHostUserId(), - room.getMembers().size(), + memberCount, room.getThumbnailUrl(), - reportReadRepository.existsUnreadReport(room.getId(), member.getUser().getId()) + reportReadRepository.existsUnreadReport(room.getId(), member.getUser().getId()), + todayPracticeSummary.todayPracticeMemberCount(), + todayPracticeSummary.todayPracticeCount() ); } public StudyRoomDetailResponse toDetailResponse(StudyRoom room, List members, - Map statsByUserId) { + Map statsByUserId, + Map todayPracticeCountsByUserId) { List memberResponses = members.stream() - .map(member -> toMemberResponse(member, statsByUserId.get(member.getUser().getId()))) + .map(member -> toMemberResponse( + member, + statsByUserId.get(member.getUser().getId()), + todayPracticeCountsByUserId.getOrDefault(member.getUser().getId(), 0) + )) .toList(); return new StudyRoomDetailResponse( room.getId(), @@ -45,7 +54,7 @@ public StudyRoomDetailResponse toDetailResponse(StudyRoom room, List getMyRooms(Long userId) { - return memberRepository.findAllWithRoomByUserId(userId).stream() - .map(mapper::toListResponse) + List memberships = memberRepository.findAllWithRoomByUserId(userId); + List roomIds = memberships.stream().map(member -> member.getRoom().getId()).toList(); + if (roomIds.isEmpty()) { + return List.of(); + } + Map memberCounts = memberRepository.countMembersByRoomIds(roomIds).stream() + .collect(java.util.stream.Collectors.toMap(row -> (Long) row[0], row -> Math.toIntExact((Long) row[1]))); + Map todayPracticeSummaries = statsService.getTodayPracticeSummariesByRoomIds(roomIds); + return memberships.stream() + .map(member -> mapper.toListResponse( + member, + memberCounts.getOrDefault(member.getRoom().getId(), 0), + todayPracticeSummaries.getOrDefault(member.getRoom().getId(), StudyRoomTodayPracticeSummary.empty()) + )) .toList(); } @@ -124,7 +137,8 @@ StudyRoomDetailResponse buildDetail(StudyRoom room) { List members = memberRepository.findAllWithUserByRoomId(room.getId()); List userIds = members.stream().map(member -> member.getUser().getId()).toList(); Map stats = statsService.getWeeklyStats(userIds); - return mapper.toDetailResponse(room, members, stats); + Map todayPracticeCounts = statsService.getTodayPracticeCounts(userIds); + return mapper.toDetailResponse(room, members, stats, todayPracticeCounts); } private String validateName(String name) { diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomStatsService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomStatsService.java index 6d4ed92c..79080841 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomStatsService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomStatsService.java @@ -1,6 +1,7 @@ package com.aisip.OnO.backend.studyroom.service; import com.aisip.OnO.backend.studyroom.dto.StudyRoomStats; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomTodayPracticeSummary; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -35,6 +36,38 @@ public Map getWeeklyStats(Collection userIds) { return getStats(userIds, start, end, today); } + public Map getTodayPracticeCounts(Collection userIds) { + LocalDate today = LocalDate.now(KST); + return countPractices(userIds, today.atStartOfDay(), today.atTime(LocalTime.MAX)); + } + + public Map getTodayPracticeSummariesByRoomIds(Collection roomIds) { + List ids = roomIds.stream().distinct().toList(); + if (ids.isEmpty()) { + return Map.of(); + } + LocalDate today = LocalDate.now(KST); + List rows = entityManager.createQuery(""" + select m.room.id, count(distinct ps.userId), count(ps.id) + from StudyRoomMember m + join ProblemSolve ps on ps.userId = m.user.id + where m.room.id in :roomIds and ps.practicedAt between :start and :end + group by m.room.id + """, Object[].class) + .setParameter("roomIds", ids) + .setParameter("start", today.atStartOfDay()) + .setParameter("end", today.atTime(LocalTime.MAX)) + .getResultList(); + Map result = new HashMap<>(); + for (Object[] row : rows) { + result.put((Long) row[0], new StudyRoomTodayPracticeSummary( + Math.toIntExact((Long) row[1]), + Math.toIntExact((Long) row[2]) + )); + } + return result; + } + public Map getStats(Collection userIds, LocalDateTime start, LocalDateTime end, LocalDate today) { List ids = userIds.stream().distinct().toList(); Map streaks = currentStreaks(ids, today); @@ -57,6 +90,23 @@ public Map getStats(Collection userIds, LocalDateTim return result; } + public Map attendanceDayCounts(Collection userIds, LocalDateTime start, LocalDateTime end) { + List ids = userIds.stream().distinct().toList(); + if (ids.isEmpty()) { + return Map.of(); + } + LocalDate today = end.toLocalDate(); + Map> studyDates = new HashMap<>(); + addStudyDates(studyDates, findProblemStudyDates(ids, today, start)); + addStudyDates(studyDates, findPracticeStudyDates(ids, today, start)); + + Map result = new HashMap<>(); + for (Long userId : ids) { + result.put(userId, studyDates.getOrDefault(userId, new TreeSet<>()).size()); + } + return result; + } + public Map currentStreaks(Collection userIds, LocalDate today) { return currentStreaks(userIds, today, null); } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomWeeklyReportService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomWeeklyReportService.java index fb8fd6f6..00f8c5cb 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomWeeklyReportService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomWeeklyReportService.java @@ -161,7 +161,10 @@ private void refreshChallengeStatuses(List roomIds, Map stats = statsService.getStats(userIds, challenge.getStartAt(), challenge.getEndAt(), streaks); - if (isChallengeCompleted(challenge, members, stats)) { + Map attendanceDays = isAttendanceMetric(challenge.getMetric()) + ? statsService.attendanceDayCounts(userIds, challenge.getStartAt(), challenge.getEndAt()) + : Map.of(); + if (isChallengeCompleted(challenge, members, stats, attendanceDays)) { challenge.updateStatus(StudyRoomChallengeStatus.COMPLETED); continue; } @@ -172,29 +175,36 @@ private void refreshChallengeStatuses(List roomIds, Map members, - Map stats) { + Map stats, Map attendanceDays) { if (members.isEmpty()) { return false; } if (challenge.getType() == StudyRoomChallengeType.GROUP) { - int groupCurrent = stats.values().stream() - .mapToInt(stat -> metricValue(challenge.getMetric(), stat)) + int groupCurrent = members.stream() + .mapToInt(member -> metricValue(challenge.getMetric(), + stats.getOrDefault(member.getUser().getId(), new StudyRoomStats(0, 0, 0)), + attendanceDays.getOrDefault(member.getUser().getId(), 0))) .sum(); return groupCurrent >= challenge.getTargetValue(); } return members.stream() .allMatch(member -> metricValue(challenge.getMetric(), - stats.getOrDefault(member.getUser().getId(), new StudyRoomStats(0, 0, 0))) >= challenge.getTargetValue()); + stats.getOrDefault(member.getUser().getId(), new StudyRoomStats(0, 0, 0)), + attendanceDays.getOrDefault(member.getUser().getId(), 0)) >= challenge.getTargetValue()); } - private int metricValue(StudyRoomChallengeMetric metric, StudyRoomStats stats) { + private int metricValue(StudyRoomChallengeMetric metric, StudyRoomStats stats, int attendanceDays) { return switch (metric) { - case WEEKLY_PROBLEM_COUNT -> stats.weeklyProblemCount(); - case WEEKLY_PRACTICE_COUNT -> stats.weeklyPracticeCount(); - case STREAK -> stats.currentStreak(); + case PROBLEM_COUNT, WEEKLY_PROBLEM_COUNT -> stats.weeklyProblemCount(); + case PRACTICE_COUNT, WEEKLY_PRACTICE_COUNT -> stats.weeklyPracticeCount(); + case ATTENDANCE, STREAK -> attendanceDays; }; } + private boolean isAttendanceMetric(StudyRoomChallengeMetric metric) { + return metric == StudyRoomChallengeMetric.ATTENDANCE || metric == StudyRoomChallengeMetric.STREAK; + } + private LocalDate min(LocalDate left, LocalDate right) { return left.isBefore(right) ? left : right; } diff --git a/src/main/resources/db/migration/V8__add_study_room_challenge_period.sql b/src/main/resources/db/migration/V8__add_study_room_challenge_period.sql new file mode 100644 index 00000000..643e8298 --- /dev/null +++ b/src/main/resources/db/migration/V8__add_study_room_challenge_period.sql @@ -0,0 +1,2 @@ +ALTER TABLE study_room_challenge + ADD COLUMN period VARCHAR(20) NULL AFTER metric; diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java index 9d62cdd8..0d212f27 100644 --- a/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java +++ b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java @@ -1,7 +1,14 @@ package com.aisip.OnO.backend.studyroom.service; import com.aisip.OnO.backend.common.exception.ApplicationException; +import com.aisip.OnO.backend.problem.dto.ProblemRegisterDto; +import com.aisip.OnO.backend.problem.entity.Problem; +import com.aisip.OnO.backend.problem.repository.ProblemRepository; +import com.aisip.OnO.backend.problemsolve.entity.AnswerStatus; +import com.aisip.OnO.backend.problemsolve.entity.ProblemSolve; +import com.aisip.OnO.backend.problemsolve.repository.ProblemSolveRepository; import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.InviteCodeResponse; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.StudyRoomListResponse; import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.StudyRoomCreateRequest; import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.StudyRoomDetailResponse; import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.StudyRoomGoalUpdateRequest; @@ -22,6 +29,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import java.time.LocalDateTime; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -49,6 +59,12 @@ class StudyRoomServiceIntegrationTest { @Autowired private UserRepository userRepository; + @Autowired + private ProblemRepository problemRepository; + + @Autowired + private ProblemSolveRepository problemSolveRepository; + @Test void createRoomCreatesHostMember() { User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); @@ -99,13 +115,14 @@ void feedReactionCanBeRecreatedAfterDelete() { StudyRoomFeedEventType.SESSION_STARTED, "{}" )); - StudyRoomFeedReaction reaction = feedReactionRepository.save(StudyRoomFeedReaction.create(feed, host, "🔥")); + StudyRoomFeedReaction reaction = feedReactionRepository.save(StudyRoomFeedReaction.create(feed, host, "fired_up_sparkle_eyes")); feedReactionRepository.delete(reaction); feedReactionRepository.flush(); - StudyRoomFeedReaction recreated = feedReactionRepository.saveAndFlush(StudyRoomFeedReaction.create(feed, host, "🔥")); + StudyRoomFeedReaction recreated = feedReactionRepository.saveAndFlush(StudyRoomFeedReaction.create(feed, host, "fired_up_sparkle_eyes")); assertThat(recreated.getId()).isNotNull(); + assertThat(recreated.getEmoji()).isEqualTo("fired_up_sparkle_eyes"); } @Test @@ -120,6 +137,49 @@ void nonMemberCannotReadRoom() { .isEqualTo(StudyRoomErrorCase.STUDY_ROOM_FORBIDDEN); } + @Test + void getMyRoomsIncludesTodayPracticeSummary() { + User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); + User member = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "멤버", "member")); + StudyRoomDetailResponse room = studyRoomService.createRoom(new StudyRoomCreateRequest("수능 준비방"), host.getId()); + InviteCodeResponse invite = inviteService.issueInviteCode(room.roomId(), host.getId()); + inviteService.join(new StudyRoomJoinRequest(invite.code()), member.getId()); + createPractice(host.getId(), 2); + createPractice(member.getId(), 3); + + List rooms = studyRoomService.getMyRooms(host.getId()); + + StudyRoomListResponse response = rooms.stream() + .filter(item -> item.roomId().equals(room.roomId())) + .findFirst() + .orElseThrow(); + assertThat(response.memberCount()).isEqualTo(2); + assertThat(response.todayPracticeMemberCount()).isEqualTo(2); + assertThat(response.todayPracticeCount()).isEqualTo(5); + } + + @Test + void getRoomIncludesMemberTodayPracticeCount() { + User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); + User member = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "멤버", "member")); + StudyRoomDetailResponse room = studyRoomService.createRoom(new StudyRoomCreateRequest("수능 준비방"), host.getId()); + InviteCodeResponse invite = inviteService.issueInviteCode(room.roomId(), host.getId()); + inviteService.join(new StudyRoomJoinRequest(invite.code()), member.getId()); + createPractice(host.getId(), 2); + createPractice(member.getId(), 0); + + StudyRoomDetailResponse response = studyRoomService.getRoom(room.roomId(), host.getId()); + + assertThat(response.members()).anySatisfy(item -> { + assertThat(item.userId()).isEqualTo(host.getId()); + assertThat(item.todayPracticeCount()).isEqualTo(2); + }); + assertThat(response.members()).anySatisfy(item -> { + assertThat(item.userId()).isEqualTo(member.getId()); + assertThat(item.todayPracticeCount()).isZero(); + }); + } + @Test void hostLeaveTransfersHostToNextMember() { User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); @@ -156,4 +216,22 @@ void updateGoalCanSetAndClearWeeklyGoal() { assertThat(studyRoomService.updateGoal(room.roomId(), host.getId(), new StudyRoomGoalUpdateRequest(0)).weeklyGoal()) .isNull(); } + + private void createPractice(Long userId, int count) { + Problem problem = problemRepository.save(Problem.from( + new ProblemRegisterDto(null, "memo", "reference", null, LocalDateTime.now()), + userId + )); + for (int i = 0; i < count; i++) { + problemSolveRepository.save(ProblemSolve.create( + problem, + userId, + LocalDateTime.now(), + AnswerStatus.CORRECT, + null, + null, + 60 + )); + } + } } From e0a5f5df09377b04750e506c9d17402416f7f59f Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sun, 7 Jun 2026 14:20:22 +0900 Subject: [PATCH 25/67] =?UTF-8?q?[Fix]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C/=EC=BF=BC=EB=A6=AC=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=20=EC=9D=B4=EB=A6=84=20=EB=AA=85=EC=8B=9C=20-=20@Path?= =?UTF-8?q?Variable/@RequestParam=EC=97=90=20name=20=EC=86=8D=EC=84=B1?= =?UTF-8?q?=EC=9D=84=20=EB=AA=85=EC=8B=9C=ED=95=B4=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EC=9D=B4=EB=A6=84=20=EC=B6=94=EB=A1=A0=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EC=9C=84=ED=97=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StudyRoomChallengeController.java | 7 ++++--- .../studyroom/controller/StudyRoomController.java | 13 +++++++------ .../controller/StudyRoomFeedController.java | 8 ++++---- .../StudyRoomSharedProblemController.java | 13 +++++++------ .../controller/StudyRoomWeeklyReportController.java | 7 ++++--- .../controller/StudySessionController.java | 7 ++++--- 6 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomChallengeController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomChallengeController.java index 7d9c4454..7566fccc 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomChallengeController.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomChallengeController.java @@ -18,19 +18,20 @@ public class StudyRoomChallengeController { private final StudyRoomChallengeService challengeService; @GetMapping - public CommonResponse> getChallenges(@PathVariable Long roomId) { + public CommonResponse> getChallenges(@PathVariable("roomId") Long roomId) { return CommonResponse.success(challengeService.getChallenges(roomId, currentUserId())); } @PostMapping @ResponseStatus(HttpStatus.CREATED) - public CommonResponse createChallenge(@PathVariable Long roomId, + public CommonResponse createChallenge(@PathVariable("roomId") Long roomId, @RequestBody ChallengeCreateRequest request) { return CommonResponse.success(challengeService.createChallenge(roomId, currentUserId(), request)); } @DeleteMapping("/{challengeId}") - public CommonResponse deleteChallenge(@PathVariable Long roomId, @PathVariable Long challengeId) { + public CommonResponse deleteChallenge(@PathVariable("roomId") Long roomId, + @PathVariable("challengeId") Long challengeId) { challengeService.deleteChallenge(roomId, challengeId, currentUserId()); return CommonResponse.success("챌린지 삭제가 완료되었습니다."); } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java index 4590bd45..bad97d80 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java @@ -31,18 +31,18 @@ public CommonResponse createRoom(@RequestBody StudyRoom } @GetMapping("/{roomId}") - public CommonResponse getRoom(@PathVariable Long roomId) { + public CommonResponse getRoom(@PathVariable("roomId") Long roomId) { return CommonResponse.success(studyRoomService.getRoom(roomId, currentUserId())); } @DeleteMapping("/{roomId}") - public CommonResponse deleteRoom(@PathVariable Long roomId) { + public CommonResponse deleteRoom(@PathVariable("roomId") Long roomId) { studyRoomService.deleteRoom(roomId, currentUserId()); return CommonResponse.success("스터디룸 삭제가 완료되었습니다."); } @PostMapping("/{roomId}/invite") - public CommonResponse issueInviteCode(@PathVariable Long roomId) { + public CommonResponse issueInviteCode(@PathVariable("roomId") Long roomId) { return CommonResponse.success(inviteService.issueInviteCode(roomId, currentUserId())); } @@ -52,19 +52,20 @@ public CommonResponse join(@RequestBody StudyRoomJoinRe } @DeleteMapping("/{roomId}/leave") - public CommonResponse leaveRoom(@PathVariable Long roomId) { + public CommonResponse leaveRoom(@PathVariable("roomId") Long roomId) { studyRoomService.leaveRoom(roomId, currentUserId()); return CommonResponse.success("스터디룸 탈퇴가 완료되었습니다."); } @DeleteMapping("/{roomId}/members/{memberId}") - public CommonResponse kickMember(@PathVariable Long roomId, @PathVariable Long memberId) { + public CommonResponse kickMember(@PathVariable("roomId") Long roomId, + @PathVariable("memberId") Long memberId) { studyRoomService.kickMember(roomId, memberId, currentUserId()); return CommonResponse.success("스터디룸 멤버 강퇴가 완료되었습니다."); } @PutMapping("/{roomId}/members/me/goal") - public CommonResponse updateGoal(@PathVariable Long roomId, + public CommonResponse updateGoal(@PathVariable("roomId") Long roomId, @RequestBody StudyRoomGoalUpdateRequest request) { return CommonResponse.success(studyRoomService.updateGoal(roomId, currentUserId(), request)); } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomFeedController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomFeedController.java index 9f886f1d..90cd3dda 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomFeedController.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomFeedController.java @@ -16,15 +16,15 @@ public class StudyRoomFeedController { private final StudyRoomFeedService feedService; @GetMapping - public CommonResponse> getFeed(@PathVariable Long roomId, + public CommonResponse> getFeed(@PathVariable("roomId") Long roomId, @RequestParam(value = "cursor", required = false) Long cursor, - @RequestParam(defaultValue = "30") int size) { + @RequestParam(value = "size", defaultValue = "30") int size) { return CommonResponse.success(feedService.getFeed(roomId, currentUserId(), cursor, size)); } @PostMapping("/{feedId}/reactions") - public CommonResponse toggleReaction(@PathVariable Long roomId, - @PathVariable Long feedId, + public CommonResponse toggleReaction(@PathVariable("roomId") Long roomId, + @PathVariable("feedId") Long feedId, @RequestBody ReactionToggleRequest request) { return CommonResponse.success(feedService.toggleReaction(roomId, feedId, currentUserId(), request)); } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomSharedProblemController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomSharedProblemController.java index 728da008..5b2c7d42 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomSharedProblemController.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomSharedProblemController.java @@ -17,28 +17,29 @@ public class StudyRoomSharedProblemController { private final StudyRoomSharedProblemService sharedProblemService; @GetMapping - public CommonResponse> getSharedProblems(@PathVariable Long roomId, + public CommonResponse> getSharedProblems(@PathVariable("roomId") Long roomId, @RequestParam(value = "cursor", required = false) Long cursor, - @RequestParam(defaultValue = "20") int size) { + @RequestParam(value = "size", defaultValue = "20") int size) { return CommonResponse.success(sharedProblemService.getSharedProblems(roomId, currentUserId(), cursor, size)); } @PostMapping @ResponseStatus(HttpStatus.CREATED) - public CommonResponse shareProblem(@PathVariable Long roomId, + public CommonResponse shareProblem(@PathVariable("roomId") Long roomId, @RequestBody SharedProblemCreateRequest request) { return CommonResponse.success(sharedProblemService.shareProblem(roomId, currentUserId(), request)); } @PostMapping("/{sharedProblemId}/reactions") - public CommonResponse toggleReaction(@PathVariable Long roomId, - @PathVariable Long sharedProblemId, + public CommonResponse toggleReaction(@PathVariable("roomId") Long roomId, + @PathVariable("sharedProblemId") Long sharedProblemId, @RequestBody ReactionToggleRequest request) { return CommonResponse.success(sharedProblemService.toggleReaction(roomId, sharedProblemId, currentUserId(), request)); } @DeleteMapping("/{sharedProblemId}") - public CommonResponse deleteSharedProblem(@PathVariable Long roomId, @PathVariable Long sharedProblemId) { + public CommonResponse deleteSharedProblem(@PathVariable("roomId") Long roomId, + @PathVariable("sharedProblemId") Long sharedProblemId) { sharedProblemService.deleteSharedProblem(roomId, sharedProblemId, currentUserId()); return CommonResponse.success("공유 문제 삭제가 완료되었습니다."); } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomWeeklyReportController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomWeeklyReportController.java index 1e7ff0c4..c29f1420 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomWeeklyReportController.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomWeeklyReportController.java @@ -17,13 +17,14 @@ public class StudyRoomWeeklyReportController { private final StudyRoomWeeklyReportService reportService; @GetMapping - public CommonResponse> getReports(@PathVariable Long roomId, - @RequestParam(defaultValue = "4") int limit) { + public CommonResponse> getReports(@PathVariable("roomId") Long roomId, + @RequestParam(value = "limit", defaultValue = "4") int limit) { return CommonResponse.success(reportService.getReports(roomId, currentUserId(), limit)); } @PatchMapping("/{reportId}/read") - public CommonResponse markRead(@PathVariable Long roomId, @PathVariable Long reportId) { + public CommonResponse markRead(@PathVariable("roomId") Long roomId, + @PathVariable("reportId") Long reportId) { return CommonResponse.success(reportService.markRead(roomId, reportId, currentUserId())); } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudySessionController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudySessionController.java index ff9fd2b1..9496e972 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudySessionController.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudySessionController.java @@ -16,18 +16,19 @@ public class StudySessionController { private final StudySessionService sessionService; @GetMapping("/active") - public CommonResponse getActiveSessions(@PathVariable Long roomId) { + public CommonResponse getActiveSessions(@PathVariable("roomId") Long roomId) { return CommonResponse.success(sessionService.getActiveSessions(roomId, currentUserId())); } @PostMapping @ResponseStatus(HttpStatus.CREATED) - public CommonResponse start(@PathVariable Long roomId) { + public CommonResponse start(@PathVariable("roomId") Long roomId) { return CommonResponse.success(sessionService.start(roomId, currentUserId())); } @PatchMapping("/{sessionId}/end") - public CommonResponse end(@PathVariable Long roomId, @PathVariable Long sessionId) { + public CommonResponse end(@PathVariable("roomId") Long roomId, + @PathVariable("sessionId") Long sessionId) { return CommonResponse.success(sessionService.end(roomId, sessionId, currentUserId())); } From c0f38152027135151865bacadc6fe8599600f017 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sun, 7 Jun 2026 23:45:25 +0900 Subject: [PATCH 26/67] =?UTF-8?q?[Feat]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20API=20=EC=B6=94=EA=B0=80=20-=20=EB=B0=A9=EC=9E=A5?= =?UTF-8?q?=EC=9D=B4=20=EB=A9=80=ED=8B=B0=ED=8C=8C=ED=8A=B8=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=EB=A5=BC=20=EC=97=85=EB=A1=9C=EB=93=9C?= =?UTF-8?q?=ED=95=B4=20=EC=8A=A4=ED=84=B0=EB=94=94=EB=A3=B8=20=EC=8D=B8?= =?UTF-8?q?=EB=84=A4=EC=9D=BC=EC=9D=84=20=EB=B3=80=EA=B2=BD=ED=95=98?= =?UTF-8?q?=EB=8A=94=20PATCH=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80=20-=20S3=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=ED=9B=84=20=EA=B8=B0=EC=A1=B4=20=EC=8D=B8=EB=84=A4?= =?UTF-8?q?=EC=9D=BC=EC=9D=80=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=ED=81=90=EB=A1=9C=20=EC=A0=95=EB=A6=AC,=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EC=9C=BC=EB=A1=9C=20=EC=83=88=20=EC=8D=B8?= =?UTF-8?q?=EB=84=A4=EC=9D=BC=20URL=20=EB=85=B8=EC=B6=9C=20-=20=EC=A0=95?= =?UTF-8?q?=EC=83=81=20=EC=97=85=EB=A1=9C=EB=93=9C/=EA=B5=90=EC=B2=B4=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B9=84=EB=B0=A9=EC=9E=A5=20=EC=A0=91=EA=B7=BC=20?= =?UTF-8?q?=EC=B0=A8=EB=8B=A8=20=EA=B2=80=EC=A6=9D=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StudyRoomController.java | 8 ++++ .../backend/studyroom/dto/StudyRoomDtos.java | 3 ++ .../backend/studyroom/entity/StudyRoom.java | 4 ++ .../studyroom/service/StudyRoomService.java | 22 +++++++++ .../StudyRoomServiceIntegrationTest.java | 46 +++++++++++++++++++ 5 files changed, 83 insertions(+) diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java index bad97d80..e1ca040d 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java @@ -6,8 +6,10 @@ import com.aisip.OnO.backend.studyroom.service.StudyRoomService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -70,6 +72,12 @@ public CommonResponse updateGoal(@PathVariable("roomId") Lon return CommonResponse.success(studyRoomService.updateGoal(roomId, currentUserId(), request)); } + @PatchMapping(value = "/{roomId}/thumbnail", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public CommonResponse updateThumbnail(@PathVariable("roomId") Long roomId, + @RequestParam("thumbnail") MultipartFile thumbnail) { + return CommonResponse.success(studyRoomService.updateThumbnail(roomId, currentUserId(), thumbnail)); + } + private Long currentUserId() { return (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java index 7f5533d5..9e05c598 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java @@ -44,6 +44,9 @@ public record StudyRoomMemberResponse(Long userId, String name, Long totalStudyL int todayPracticeCount) { } + public record StudyRoomThumbnailUpdateResponse(String thumbnailUrl) { + } + public record InviteCodeResponse(String code, LocalDateTime expiredAt) { } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoom.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoom.java index 0418e43d..6a75cf15 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoom.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoom.java @@ -53,4 +53,8 @@ public void addMember(StudyRoomMember member) { public void updateHostUserId(Long hostUserId) { this.hostUserId = hostUserId; } + + public void updateThumbnailUrl(String thumbnailUrl) { + this.thumbnailUrl = thumbnailUrl; + } } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java index 30a10171..c23a9304 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java @@ -10,12 +10,16 @@ import com.aisip.OnO.backend.studyroom.exception.StudyRoomErrorCase; import com.aisip.OnO.backend.studyroom.repository.StudyRoomMemberRepository; import com.aisip.OnO.backend.studyroom.repository.StudyRoomRepository; +import com.aisip.OnO.backend.config.rabbitmq.producer.S3DeleteProducer; import com.aisip.OnO.backend.user.entity.User; import com.aisip.OnO.backend.user.exception.UserErrorCase; import com.aisip.OnO.backend.user.repository.UserRepository; +import com.aisip.OnO.backend.util.fileupload.exception.FileUploadErrorCase; +import com.aisip.OnO.backend.util.fileupload.service.FileUploadService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import java.util.List; import java.util.Map; @@ -33,6 +37,8 @@ public class StudyRoomService { private final StudyRoomAccessService accessService; private final StudyRoomStatsService statsService; private final StudyRoomMapper mapper; + private final FileUploadService fileUploadService; + private final S3DeleteProducer s3DeleteProducer; @Transactional(readOnly = true) public List getMyRooms(Long userId) { @@ -133,6 +139,22 @@ public GoalUpdateResponse updateGoal(Long roomId, Long userId, StudyRoomGoalUpda return new GoalUpdateResponse(member.getWeeklyGoal(), member.getWeeklyGoal() == null ? null : stats.weeklyProblemCount()); } + @Transactional + public StudyRoomThumbnailUpdateResponse updateThumbnail(Long roomId, Long userId, MultipartFile thumbnail) { + accessService.validateHost(roomId, userId); + if (thumbnail == null || thumbnail.isEmpty()) { + throw new ApplicationException(FileUploadErrorCase.FILE_UPLOAD_FAILED); + } + StudyRoom room = accessService.getRoomOrThrow(roomId); + String previousThumbnailUrl = room.getThumbnailUrl(); + String thumbnailUrl = fileUploadService.uploadFileToS3(thumbnail); + room.updateThumbnailUrl(thumbnailUrl); + if (previousThumbnailUrl != null && !previousThumbnailUrl.isBlank()) { + s3DeleteProducer.sendDeleteMessage(previousThumbnailUrl, roomId); + } + return new StudyRoomThumbnailUpdateResponse(thumbnailUrl); + } + StudyRoomDetailResponse buildDetail(StudyRoom room) { List members = memberRepository.findAllWithUserByRoomId(room.getId()); List userIds = members.stream().map(member -> member.getUser().getId()).toList(); diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java index 0d212f27..835207b6 100644 --- a/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java +++ b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java @@ -1,6 +1,7 @@ package com.aisip.OnO.backend.studyroom.service; import com.aisip.OnO.backend.common.exception.ApplicationException; +import com.aisip.OnO.backend.config.rabbitmq.producer.S3DeleteProducer; import com.aisip.OnO.backend.problem.dto.ProblemRegisterDto; import com.aisip.OnO.backend.problem.entity.Problem; import com.aisip.OnO.backend.problem.repository.ProblemRepository; @@ -25,15 +26,20 @@ import com.aisip.OnO.backend.user.entity.User; import com.aisip.OnO.backend.user.repository.UserRepository; import com.aisip.OnO.backend.util.RandomUserGenerator; +import com.aisip.OnO.backend.util.fileupload.service.FileUploadService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockMultipartFile; import java.time.LocalDateTime; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; @SpringBootTest class StudyRoomServiceIntegrationTest { @@ -65,6 +71,12 @@ class StudyRoomServiceIntegrationTest { @Autowired private ProblemSolveRepository problemSolveRepository; + @MockBean + private FileUploadService fileUploadService; + + @MockBean + private S3DeleteProducer s3DeleteProducer; + @Test void createRoomCreatesHostMember() { User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); @@ -180,6 +192,40 @@ void getRoomIncludesMemberTodayPracticeCount() { }); } + @Test + void updateThumbnailUploadsAndStoresThumbnailUrl() { + User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); + StudyRoomDetailResponse room = studyRoomService.createRoom(new StudyRoomCreateRequest("수능 준비방"), host.getId()); + MockMultipartFile firstThumbnail = new MockMultipartFile("thumbnail", "first.png", "image/png", "first".getBytes()); + MockMultipartFile secondThumbnail = new MockMultipartFile("thumbnail", "second.png", "image/png", "second".getBytes()); + given(fileUploadService.uploadFileToS3(firstThumbnail)).willReturn("https://cdn.example.com/first.png"); + given(fileUploadService.uploadFileToS3(secondThumbnail)).willReturn("https://cdn.example.com/second.png"); + + assertThat(studyRoomService.updateThumbnail(room.roomId(), host.getId(), firstThumbnail).thumbnailUrl()) + .isEqualTo("https://cdn.example.com/first.png"); + assertThat(studyRoomService.updateThumbnail(room.roomId(), host.getId(), secondThumbnail).thumbnailUrl()) + .isEqualTo("https://cdn.example.com/second.png"); + + assertThat(roomRepository.findById(room.roomId()).orElseThrow().getThumbnailUrl()) + .isEqualTo("https://cdn.example.com/second.png"); + verify(s3DeleteProducer).sendDeleteMessage("https://cdn.example.com/first.png", room.roomId()); + } + + @Test + void nonHostCannotUpdateThumbnail() { + User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); + User member = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "멤버", "member")); + StudyRoomDetailResponse room = studyRoomService.createRoom(new StudyRoomCreateRequest("수능 준비방"), host.getId()); + InviteCodeResponse invite = inviteService.issueInviteCode(room.roomId(), host.getId()); + inviteService.join(new StudyRoomJoinRequest(invite.code()), member.getId()); + MockMultipartFile thumbnail = new MockMultipartFile("thumbnail", "first.png", "image/png", "first".getBytes()); + + assertThatThrownBy(() -> studyRoomService.updateThumbnail(room.roomId(), member.getId(), thumbnail)) + .isInstanceOf(ApplicationException.class) + .extracting("errorCase") + .isEqualTo(StudyRoomErrorCase.STUDY_ROOM_HOST_ONLY); + } + @Test void hostLeaveTransfersHostToNextMember() { User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); From 0c1a4ba4f0d0e59c3e840bc9a34ad07d972b0027 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sun, 7 Jun 2026 23:45:30 +0900 Subject: [PATCH 27/67] =?UTF-8?q?[Fix]=20=EA=B3=B5=EC=9C=A0=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=82=AD=EC=A0=9C=20=EC=8B=9C=20=EC=9E=94=EC=97=AC?= =?UTF-8?q?=20=EB=B0=98=EC=9D=91=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=20-=20=EA=B3=B5=EC=9C=A0=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=A0=84=20=ED=95=B4=EB=8B=B9=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=EC=97=90=20=EB=8B=AC=EB=A6=B0=20=EB=B0=98=EC=9D=91?= =?UTF-8?q?=EC=9D=84=20=ED=95=A8=EA=BB=98=20=EC=82=AD=EC=A0=9C=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A6=AC=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80=20-=20?= =?UTF-8?q?=EC=99=B8=EB=9E=98=ED=82=A4=20=EC=A0=9C=EC=95=BD=20=EC=9C=84?= =?UTF-8?q?=EB=B0=98=20=EC=97=86=EC=9D=B4=20=EA=B3=B5=EC=9C=A0=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=EB=A5=BC=20=EC=82=AD=EC=A0=9C=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/StudyRoomSharedProblemReactionRepository.java | 2 ++ .../studyroom/service/StudyRoomSharedProblemService.java | 1 + 2 files changed, 3 insertions(+) diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemReactionRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemReactionRepository.java index f4ecc4f2..4b28152f 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemReactionRepository.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemReactionRepository.java @@ -17,4 +17,6 @@ public interface StudyRoomSharedProblemReactionRepository extends JpaRepository< List findAllBySharedProblemIds(@Param("sharedProblemIds") Collection sharedProblemIds); List findAllBySharedProblemId(Long sharedProblemId); + + void deleteBySharedProblemId(Long sharedProblemId); } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java index dea70a29..09b35012 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java @@ -98,6 +98,7 @@ public void deleteSharedProblem(Long roomId, Long sharedProblemId, Long userId) if (!sharedProblem.getSharedByUser().getId().equals(userId)) { throw new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_FORBIDDEN); } + reactionRepository.deleteBySharedProblemId(sharedProblemId); sharedProblemRepository.delete(sharedProblem); } From 54819d076782a84eaf6b7ee65dc06a3dd34de90f Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sun, 7 Jun 2026 23:45:36 +0900 Subject: [PATCH 28/67] =?UTF-8?q?[Feat]=20=EC=B1=8C=EB=A6=B0=EC=A7=80=20?= =?UTF-8?q?=EA=B8=B0=EA=B0=84(period)=20=EA=B8=B0=EC=A4=80=20=ED=98=84?= =?UTF-8?q?=EC=9E=AC=20=EA=B5=AC=EA=B0=84=20=EC=A7=91=EA=B3=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20DAILY/WEEKLY/MONTHLY=20=EC=A3=BC=EA=B8=B0=20?= =?UTF-8?q?=EC=B1=8C=EB=A6=B0=EC=A7=80=EC=9D=98=20=EC=A7=84=ED=96=89=20?= =?UTF-8?q?=EC=83=81=ED=99=A9=EC=9D=84=20=EC=A0=84=EC=B2=B4=20=EA=B8=B0?= =?UTF-8?q?=EA=B0=84=EC=9D=B4=20=EC=95=84=EB=8B=8C=20=ED=98=84=EC=9E=AC=20?= =?UTF-8?q?=EA=B5=AC=EA=B0=84=20=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A7=91=EA=B3=84=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20-=20=EC=A3=BC=EA=B8=B0=ED=98=95=20=EC=B1=8C?= =?UTF-8?q?=EB=A6=B0=EC=A7=80=EB=8A=94=20=EA=B7=B8=EB=A3=B9=C2=B7=EA=B0=9C?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A9=ED=91=9C=20=EB=8B=AC=EC=84=B1=EB=A7=8C?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=A1=B0=EA=B8=B0=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=EB=90=98=EC=A7=80=20=EC=95=8A=EA=B3=A0=20?= =?UTF-8?q?=EC=A2=85=EB=A3=8C=20=EC=8B=9C=EC=A0=90=EA=B9=8C=EC=A7=80=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=20=EC=83=81=ED=83=9C=20=EC=9C=A0=EC=A7=80=20?= =?UTF-8?q?-=20=EC=A7=91=EA=B3=84=20=EA=B5=AC=EA=B0=84=20=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=20=EB=A1=9C=EC=A7=81=20=EA=B2=80=EC=A6=9D=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/StudyRoomChallengeService.java | 54 ++++++- .../StudyRoomChallengeServiceTest.java | 136 ++++++++++++++++++ 2 files changed, 183 insertions(+), 7 deletions(-) create mode 100644 src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeServiceTest.java diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java index 45714533..5980e2de 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java @@ -79,11 +79,12 @@ private void refreshRoomChallengeStatuses(Long roomId) { private ChallengeResponse toResponse(StudyRoomChallenge challenge, List members, boolean persistStatus) { List userIds = members.stream().map(member -> member.getUser().getId()).toList(); + AggregationRange range = resolveAggregationRange(challenge, LocalDateTime.now()); LocalDate streakBaseDate = min(LocalDate.now(), challenge.getEndAt().toLocalDate()); Map streaks = statsService.currentStreaks(userIds, streakBaseDate, challenge.getStartAt().toLocalDate()); - Map stats = statsService.getStats(userIds, challenge.getStartAt(), challenge.getEndAt(), streaks); + Map stats = statsService.getStats(userIds, range.start(), range.end(), streaks); Map attendanceDays = isAttendanceMetric(challenge.getMetric()) - ? statsService.attendanceDayCounts(userIds, challenge.getStartAt(), challenge.getEndAt()) + ? statsService.attendanceDayCounts(userIds, range.start(), range.end()) : Map.of(); List memberProgress = challenge.getType() == StudyRoomChallengeType.GROUP ? List.of() @@ -126,11 +127,13 @@ private StudyRoomChallengeStatus effectiveStatus(StudyRoomChallenge challenge, if (challenge.getStatus() != StudyRoomChallengeStatus.IN_PROGRESS) { return challenge.getStatus(); } - boolean completed = challenge.getType() == StudyRoomChallengeType.GROUP - ? groupCurrent != null && groupCurrent >= challenge.getTargetValue() - : !memberProgress.isEmpty() && memberProgress.stream().allMatch(ChallengeMemberProgressResponse::cleared); - if (completed) { - return StudyRoomChallengeStatus.COMPLETED; + if (challenge.getPeriod() == null) { + boolean completed = challenge.getType() == StudyRoomChallengeType.GROUP + ? groupCurrent != null && groupCurrent >= challenge.getTargetValue() + : !memberProgress.isEmpty() && memberProgress.stream().allMatch(ChallengeMemberProgressResponse::cleared); + if (completed) { + return StudyRoomChallengeStatus.COMPLETED; + } } if (challenge.getEndAt().isBefore(LocalDateTime.now())) { return StudyRoomChallengeStatus.EXPIRED; @@ -153,6 +156,43 @@ private boolean isAttendanceMetric(StudyRoomChallengeMetric metric) { return metric == StudyRoomChallengeMetric.ATTENDANCE || metric == StudyRoomChallengeMetric.STREAK; } + private AggregationRange resolveAggregationRange(StudyRoomChallenge challenge, LocalDateTime now) { + LocalDateTime startAt = challenge.getStartAt(); + LocalDateTime endAt = challenge.getEndAt(); + StudyRoomChallengePeriod period = challenge.getPeriod(); + if (period == null) { + return new AggregationRange(startAt, endAt); + } + LocalDateTime anchor = clamp(now, startAt, endAt); + LocalDateTime windowStart = startAt; + LocalDateTime windowEnd = addPeriod(windowStart, period); + while (!windowEnd.isAfter(anchor) && windowEnd.isBefore(endAt)) { + windowStart = windowEnd; + windowEnd = addPeriod(windowStart, period); + } + return new AggregationRange(windowStart, windowEnd.isAfter(endAt) ? endAt : windowEnd); + } + + private LocalDateTime addPeriod(LocalDateTime base, StudyRoomChallengePeriod period) { + return switch (period) { + case DAILY -> base.plusDays(1); + case WEEKLY -> base.plusDays(7); + case MONTHLY -> base.plusMonths(1); + }; + } + + private LocalDateTime clamp(LocalDateTime value, LocalDateTime lower, LocalDateTime upper) { + if (value.isBefore(lower)) { + return lower; + } + if (value.isAfter(upper)) { + return upper; + } + return value; + } + + private record AggregationRange(LocalDateTime start, LocalDateTime end) {} + private void validateCreateRequest(ChallengeCreateRequest request) { if (request.title() == null || request.title().isBlank() || request.title().length() > 40 || request.targetValue() == null || request.targetValue() < 1 diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeServiceTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeServiceTest.java new file mode 100644 index 00000000..0488338a --- /dev/null +++ b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeServiceTest.java @@ -0,0 +1,136 @@ +package com.aisip.OnO.backend.studyroom.service; + +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.ChallengeResponse; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomStats; +import com.aisip.OnO.backend.studyroom.entity.*; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomChallengeRepository; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomMemberRepository; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.util.RandomUserGenerator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.springframework.test.util.ReflectionTestUtils.setField; + +@ExtendWith(MockitoExtension.class) +class StudyRoomChallengeServiceTest { + + @Mock + private StudyRoomAccessService accessService; + + @Mock + private StudyRoomChallengeRepository challengeRepository; + + @Mock + private StudyRoomMemberRepository memberRepository; + + @Mock + private StudyRoomStatsService statsService; + + private StudyRoomChallengeService challengeService; + + @BeforeEach + void setUp() { + challengeService = new StudyRoomChallengeService(accessService, challengeRepository, memberRepository, statsService); + } + + @Test + void weeklyPeriodChallengeUsesCurrentPeriodRangeAndDoesNotCompleteEarly() { + LocalDateTime startAt = LocalDateTime.now().minusDays(10).truncatedTo(ChronoUnit.SECONDS); + LocalDateTime endAt = startAt.plusDays(21); + User user = user(1L, "멤버"); + StudyRoomMember member = StudyRoomMember.create(user, StudyRoomMemberRole.HOST); + StudyRoomChallenge challenge = challenge( + StudyRoomChallengeType.INDIVIDUAL, + StudyRoomChallengeMetric.PRACTICE_COUNT, + StudyRoomChallengePeriod.WEEKLY, + 3, + startAt, + endAt + ); + + given(memberRepository.findAllWithUserByRoomId(10L)).willReturn(List.of(member)); + given(challengeRepository.findActiveByRoomId(eq(10L), eq(StudyRoomChallengeStatus.IN_PROGRESS), any(LocalDateTime.class))) + .willReturn(List.of(challenge)); + given(statsService.currentStreaks(anyList(), any(), any())).willReturn(Map.of(user.getId(), 0)); + given(statsService.getStats(anyList(), any(LocalDateTime.class), any(LocalDateTime.class), anyMap())) + .willReturn(Map.of(user.getId(), new StudyRoomStats(0, 3, 0))); + + List responses = challengeService.getChallenges(10L, user.getId()); + + assertThat(responses).hasSize(1); + assertThat(responses.get(0).status()).isEqualTo("in_progress"); + assertThat(responses.get(0).memberProgress()).singleElement().satisfies(progress -> { + assertThat(progress.current()).isEqualTo(3); + assertThat(progress.cleared()).isTrue(); + }); + ArgumentCaptor rangeStart = ArgumentCaptor.forClass(LocalDateTime.class); + ArgumentCaptor rangeEnd = ArgumentCaptor.forClass(LocalDateTime.class); + verify(statsService).getStats(anyList(), rangeStart.capture(), rangeEnd.capture(), anyMap()); + assertThat(rangeStart.getValue()).isEqualTo(startAt.plusWeeks(1)); + assertThat(rangeEnd.getValue()).isEqualTo(startAt.plusWeeks(2)); + } + + @Test + void nonPeriodGroupChallengeCompletesWhenGroupCurrentReachesTarget() { + LocalDateTime startAt = LocalDateTime.now().minusDays(1).truncatedTo(ChronoUnit.SECONDS); + LocalDateTime endAt = LocalDateTime.now().plusDays(1).truncatedTo(ChronoUnit.SECONDS); + User first = user(1L, "첫번째"); + User second = user(2L, "두번째"); + StudyRoomChallenge challenge = challenge( + StudyRoomChallengeType.GROUP, + StudyRoomChallengeMetric.PRACTICE_COUNT, + null, + 5, + startAt, + endAt + ); + + given(memberRepository.findAllWithUserByRoomId(10L)) + .willReturn(List.of(StudyRoomMember.create(first, StudyRoomMemberRole.HOST), + StudyRoomMember.create(second, StudyRoomMemberRole.MEMBER))); + given(challengeRepository.findActiveByRoomId(eq(10L), eq(StudyRoomChallengeStatus.IN_PROGRESS), any(LocalDateTime.class))) + .willReturn(List.of(challenge)); + given(statsService.currentStreaks(anyList(), any(), any())).willReturn(Map.of(first.getId(), 0, second.getId(), 0)); + given(statsService.getStats(anyList(), any(LocalDateTime.class), any(LocalDateTime.class), anyMap())) + .willReturn(Map.of( + first.getId(), new StudyRoomStats(0, 2, 0), + second.getId(), new StudyRoomStats(0, 3, 0) + )); + + ChallengeResponse response = challengeService.getChallenges(10L, first.getId()).get(0); + + assertThat(response.status()).isEqualTo("completed"); + assertThat(response.groupCurrent()).isEqualTo(5); + } + + private StudyRoomChallenge challenge(StudyRoomChallengeType type, StudyRoomChallengeMetric metric, + StudyRoomChallengePeriod period, int targetValue, + LocalDateTime startAt, LocalDateTime endAt) { + StudyRoom room = StudyRoom.create("테스트방", 1L); + setField(room, "id", 10L); + StudyRoomChallenge challenge = StudyRoomChallenge.create(room, "테스트 챌린지", type, metric, period, + targetValue, startAt, endAt); + setField(challenge, "id", 100L); + return challenge; + } + + private User user(Long id, String name) { + User user = RandomUserGenerator.createRandomUser("GOOGLE", name, "study-room-service-test"); + setField(user, "id", id); + return user; + } +} From 7dc4a23c180b77d2344b1e394e7ba4050b33e21c Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sun, 7 Jun 2026 23:45:43 +0900 Subject: [PATCH 29/67] =?UTF-8?q?[Test]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20API=20=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80=20-=20MockMvc=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EB=B0=A9=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=C2=B7=EC=B4=88=EB=8C=80=C2=B7=EC=B0=B8=EC=97=AC=C2=B7=ED=94=BC?= =?UTF-8?q?=EB=93=9C=20=EB=B0=98=EC=9D=91=C2=B7=EA=B3=B5=EC=9C=A0=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EB=B0=98=EC=9D=91/=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=C2=B7=ED=95=99=EC=8A=B5=20=EC=84=B8=EC=85=98=20=ED=9D=90?= =?UTF-8?q?=EB=A6=84=EC=9D=84=20=EC=97=94=EB=93=9C=ED=88=AC=EC=97=94?= =?UTF-8?q?=EB=93=9C=20=EA=B2=80=EC=A6=9D=20-=20=EC=A3=BC=EA=B8=B0?= =?UTF-8?q?=ED=98=95=20=EC=B1=8C=EB=A6=B0=EC=A7=80=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20=ED=98=84=EC=9E=AC=20=EA=B5=AC=EA=B0=84=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=20=EC=A7=84=ED=96=89=20=EC=83=81=ED=99=A9=20=EC=A7=91?= =?UTF-8?q?=EA=B3=84=EB=A5=BC=20API=20=EB=A0=88=EB=B2=A8=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=ED=95=98=EB=8A=94=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B6=84=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StudyRoomApiIntegrationTest.java | 376 ++++++++++++++++++ .../StudyRoomChallengeApiIntegrationTest.java | 183 +++++++++ 2 files changed, 559 insertions(+) create mode 100644 src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java create mode 100644 src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomChallengeApiIntegrationTest.java diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java new file mode 100644 index 00000000..89406e43 --- /dev/null +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java @@ -0,0 +1,376 @@ +package com.aisip.OnO.backend.studyroom.integration; + +import com.aisip.OnO.backend.problem.dto.ProblemRegisterDto; +import com.aisip.OnO.backend.problem.entity.Problem; +import com.aisip.OnO.backend.problem.repository.ProblemRepository; +import com.aisip.OnO.backend.problemsolve.entity.AnswerStatus; +import com.aisip.OnO.backend.problemsolve.entity.ProblemSolve; +import com.aisip.OnO.backend.problemsolve.repository.ProblemSolveRepository; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.studyroom.entity.StudyRoom; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomFeed; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomFeedEventType; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomFeedRepository; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomRepository; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.repository.UserRepository; +import com.aisip.OnO.backend.util.RandomUserGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +@ActiveProfiles("test") +class StudyRoomApiIntegrationTest { + + private static final String EMOJI = "fired_up_sparkle_eyes"; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private UserRepository userRepository; + + @Autowired + private StudyRoomRepository roomRepository; + + @Autowired + private StudyRoomFeedRepository feedRepository; + + @Autowired + private ProblemRepository problemRepository; + + @Autowired + private ProblemSolveRepository problemSolveRepository; + + private Long authenticatedUserId; + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void roomCreateInviteJoinReadAndTodayPracticeSummaryApiFlow() throws Exception { + User host = saveUser("방장", "room-flow-host"); + User member = saveUser("멤버", "room-flow-member"); + authenticate(host.getId()); + + Long roomId = createRoom("수능 준비방"); + createPractice(host.getId(), LocalDateTime.now(), 2); + + String inviteCode = issueInviteCode(roomId); + authenticate(member.getId()); + mockMvc.perform(post("/api/study-room/join") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomJoinRequest(inviteCode)))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.roomId").value(roomId)) + .andExpect(jsonPath("$.data.memberCount").value(2)); + createPractice(member.getId(), LocalDateTime.now(), 3); + + mockMvc.perform(get("/api/study-room/{roomId}", roomId) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.memberCount").value(2)) + .andExpect(jsonPath("$.data.members[?(@.userId == " + host.getId() + ")].todayPracticeCount").value(2)) + .andExpect(jsonPath("$.data.members[?(@.userId == " + member.getId() + ")].todayPracticeCount").value(3)); + + mockMvc.perform(get("/api/study-room") + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data[?(@.roomId == " + roomId + ")].todayPracticeMemberCount").value(2)) + .andExpect(jsonPath("$.data[?(@.roomId == " + roomId + ")].todayPracticeCount").value(5)); + } + + @Test + void nonMemberCannotReadRoomAndMemberCannotKickOtherMember() throws Exception { + User host = saveUser("방장", "permission-host"); + User member = saveUser("멤버", "permission-member"); + User outsider = saveUser("외부", "permission-outsider"); + authenticate(host.getId()); + Long roomId = createRoom("권한 검증방"); + String inviteCode = issueInviteCode(roomId); + authenticate(member.getId()); + join(inviteCode); + + authenticate(outsider.getId()); + mockMvc.perform(get("/api/study-room/{roomId}", roomId) + .with(auth())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(10002)); + + authenticate(member.getId()); + mockMvc.perform(delete("/api/study-room/{roomId}/members/{memberId}", roomId, host.getId()) + .with(auth())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(10003)); + } + + @Test + void feedReactionToggleAndInvalidEmojiAreValidatedByApi() throws Exception { + User host = saveUser("방장", "feed-host"); + authenticate(host.getId()); + Long roomId = createRoom("피드 반응방"); + StudyRoom room = roomRepository.findById(roomId).orElseThrow(); + StudyRoomFeed feed = feedRepository.save(StudyRoomFeed.create(room, host, StudyRoomFeedEventType.SESSION_STARTED, "{}")); + + mockMvc.perform(post("/api/study-room/{roomId}/feed/{feedId}/reactions", roomId, feed.getId()) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ReactionToggleRequest(EMOJI)))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.feedId").value(feed.getId())) + .andExpect(jsonPath("$.data.reactions[0].emoji").value(EMOJI)) + .andExpect(jsonPath("$.data.reactions[0].count").value(1)) + .andExpect(jsonPath("$.data.reactions[0].reactedByMe").value(true)); + + mockMvc.perform(post("/api/study-room/{roomId}/feed/{feedId}/reactions", roomId, feed.getId()) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ReactionToggleRequest(EMOJI)))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.reactions").isEmpty()); + + mockMvc.perform(post("/api/study-room/{roomId}/feed/{feedId}/reactions", roomId, feed.getId()) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ReactionToggleRequest("not_allowed")))) + .andExpect(status().isBadRequest()); + } + + @Test + void sharedProblemShareReactionAndDeleteAuthorizationApiFlow() throws Exception { + User host = saveUser("방장", "shared-host"); + User member = saveUser("멤버", "shared-member"); + authenticate(host.getId()); + Long roomId = createRoom("공유 문제방"); + String inviteCode = issueInviteCode(roomId); + Problem problem = createProblem(host.getId()); + authenticate(member.getId()); + join(inviteCode); + + authenticate(host.getId()); + Long sharedProblemId = shareProblem(roomId, problem.getId(), "같이 보면 좋은 문제"); + + authenticate(member.getId()); + mockMvc.perform(post("/api/study-room/{roomId}/shared-problems/{sharedProblemId}/reactions", roomId, sharedProblemId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ReactionToggleRequest(EMOJI)))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.sharedProblemId").value(sharedProblemId)) + .andExpect(jsonPath("$.data.reactions[0].count").value(1)) + .andExpect(jsonPath("$.data.reactions[0].reactedByMe").value(true)); + + mockMvc.perform(delete("/api/study-room/{roomId}/shared-problems/{sharedProblemId}", roomId, sharedProblemId) + .with(auth())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(10002)); + + authenticate(host.getId()); + mockMvc.perform(delete("/api/study-room/{roomId}/shared-problems/{sharedProblemId}", roomId, sharedProblemId) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data").value("공유 문제 삭제가 완료되었습니다.")); + } + + @Test + void studySessionStartActiveDuplicateAndEndApiFlow() throws Exception { + User host = saveUser("방장", "session-host"); + authenticate(host.getId()); + Long roomId = createRoom("세션방"); + + Long sessionId = startSession(roomId); + mockMvc.perform(get("/api/study-room/{roomId}/sessions/active", roomId) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.activeSessions[0].userId").value(host.getId())); + + mockMvc.perform(post("/api/study-room/{roomId}/sessions", roomId) + .with(auth())) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.errorCode").value(10011)); + + mockMvc.perform(patch("/api/study-room/{roomId}/sessions/{sessionId}/end", roomId, sessionId) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.sessionId").value(sessionId)) + .andExpect(jsonPath("$.data.endedAt").exists()); + + mockMvc.perform(get("/api/study-room/{roomId}/sessions/active", roomId) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.activeSessions").isEmpty()); + } + + @Test + void challengeApiUsesCurrentPeriodPracticeCountsAndKeepsPeriodChallengeInProgress() throws Exception { + User host = saveUser("방장", "challenge-host"); + authenticate(host.getId()); + Long roomId = createRoom("챌린지방"); + LocalDateTime startAt = LocalDateTime.now().minusDays(10).truncatedTo(ChronoUnit.SECONDS); + LocalDateTime endAt = startAt.plusWeeks(3); + + mockMvc.perform(post("/api/study-room/{roomId}/challenges", roomId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ChallengeCreateRequest( + "이번 주 복습 2회", + "individual", + "practice_count", + "weekly", + 2, + startAt, + endAt + )))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.data.period").value("weekly")); + + createPractice(host.getId(), startAt.plusDays(1), 4); + createPractice(host.getId(), startAt.plusWeeks(1).plusHours(1), 2); + + mockMvc.perform(get("/api/study-room/{roomId}/challenges", roomId) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data[0].status").value("in_progress")) + .andExpect(jsonPath("$.data[0].memberProgress[0].current").value(2)) + .andExpect(jsonPath("$.data[0].memberProgress[0].cleared").value(true)); + + mockMvc.perform(post("/api/study-room/{roomId}/challenges", roomId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ChallengeCreateRequest( + " ", + "individual", + "practice_count", + "weekly", + 2, + startAt, + endAt + )))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value(10016)); + } + + private User saveUser(String name, String emailPrefix) { + return userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", name, emailPrefix)); + } + + private void authenticate(Long userId) { + authenticatedUserId = userId; + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken( + userId, + null, + List.of(new SimpleGrantedAuthority("ROLE_MEMBER")) + ) + ); + } + + private RequestPostProcessor auth() { + return authentication(new UsernamePasswordAuthenticationToken( + authenticatedUserId, + null, + List.of(new SimpleGrantedAuthority("ROLE_MEMBER")) + )); + } + + private Long createRoom(String name) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomCreateRequest(name)))) + .andExpect(status().isCreated()) + .andReturn(); + return readLong(result, "$.data.roomId"); + } + + private String issueInviteCode(Long roomId) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room/{roomId}/invite", roomId) + .with(auth())) + .andExpect(status().isOk()) + .andReturn(); + return JsonPath.read(result.getResponse().getContentAsString(), "$.data.code"); + } + + private void join(String inviteCode) throws Exception { + mockMvc.perform(post("/api/study-room/join") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomJoinRequest(inviteCode)))) + .andExpect(status().isOk()); + } + + private Long startSession(Long roomId) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room/{roomId}/sessions", roomId) + .with(auth())) + .andExpect(status().isCreated()) + .andReturn(); + return readLong(result, "$.data.sessionId"); + } + + private Long shareProblem(Long roomId, Long problemId, String comment) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room/{roomId}/shared-problems", roomId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new SharedProblemCreateRequest(problemId, comment)))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.data.problemId").value(problemId)) + .andReturn(); + return readLong(result, "$.data.sharedProblemId"); + } + + private Long readLong(MvcResult result, String path) throws Exception { + Number value = JsonPath.read(result.getResponse().getContentAsString(), path); + return value.longValue(); + } + + private Problem createProblem(Long userId) { + return problemRepository.save(Problem.from( + new ProblemRegisterDto(null, "memo", "reference", null, LocalDateTime.now()), + userId + )); + } + + private void createPractice(Long userId, LocalDateTime practicedAt, int count) { + Problem problem = createProblem(userId); + for (int i = 0; i < count; i++) { + problemSolveRepository.save(ProblemSolve.create( + problem, + userId, + practicedAt.plusSeconds(i), + AnswerStatus.CORRECT, + null, + null, + 60 + )); + } + } +} diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomChallengeApiIntegrationTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomChallengeApiIntegrationTest.java new file mode 100644 index 00000000..ff8a0e47 --- /dev/null +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomChallengeApiIntegrationTest.java @@ -0,0 +1,183 @@ +package com.aisip.OnO.backend.studyroom.integration; + +import com.aisip.OnO.backend.problem.dto.ProblemRegisterDto; +import com.aisip.OnO.backend.problem.entity.Problem; +import com.aisip.OnO.backend.problem.repository.ProblemRepository; +import com.aisip.OnO.backend.problemsolve.entity.AnswerStatus; +import com.aisip.OnO.backend.problemsolve.entity.ProblemSolve; +import com.aisip.OnO.backend.problemsolve.repository.ProblemSolveRepository; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.ChallengeCreateRequest; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.InviteCodeResponse; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.StudyRoomCreateRequest; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.StudyRoomDetailResponse; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.StudyRoomJoinRequest; +import com.aisip.OnO.backend.studyroom.service.StudyRoomInviteService; +import com.aisip.OnO.backend.studyroom.service.StudyRoomService; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.repository.UserRepository; +import com.aisip.OnO.backend.util.RandomUserGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +@ActiveProfiles("test") +class StudyRoomChallengeApiIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private StudyRoomService studyRoomService; + + @Autowired + private StudyRoomInviteService inviteService; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ProblemRepository problemRepository; + + @Autowired + private ProblemSolveRepository problemSolveRepository; + + private User host; + private User member; + private User outsider; + private Long roomId; + + @BeforeEach + void setUp() { + host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "challenge-host")); + member = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "멤버", "challenge-member")); + outsider = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "외부", "challenge-outsider")); + + authenticate(host.getId()); + StudyRoomDetailResponse room = studyRoomService.createRoom(new StudyRoomCreateRequest("챌린지 테스트방"), host.getId()); + InviteCodeResponse invite = inviteService.issueInviteCode(room.roomId(), host.getId()); + inviteService.join(new StudyRoomJoinRequest(invite.code()), member.getId()); + roomId = room.roomId(); + } + + @Test + void memberCanCreateWeeklyChallengeAndProgressCountsOnlyCurrentPeriod() throws Exception { + LocalDateTime startAt = LocalDateTime.now().minusDays(10).withNano(0); + LocalDateTime currentPeriodStart = startAt.plusWeeks(1); + createPractice(member.getId(), startAt.plusDays(2)); + createPractice(member.getId(), currentPeriodStart.plusDays(1)); + createPractice(member.getId(), currentPeriodStart.plusDays(2)); + ChallengeCreateRequest request = new ChallengeCreateRequest( + "이번 주 복습 2개", + "individual", + "practice_count", + "weekly", + 2, + startAt, + startAt.plusWeeks(3) + ); + + authenticate(member.getId()); + MvcResult result = mockMvc.perform(post("/api/study-room/{roomId}/challenges", roomId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.data.title").value("이번 주 복습 2개")) + .andExpect(jsonPath("$.data.type").value("individual")) + .andExpect(jsonPath("$.data.metric").value("practice_count")) + .andExpect(jsonPath("$.data.period").value("weekly")) + .andExpect(jsonPath("$.data.status").value("in_progress")) + .andReturn(); + + List> memberProgress = JsonPath.read( + result.getResponse().getContentAsString(), + "$.data.memberProgress" + ); + Map targetMemberProgress = memberProgress.stream() + .filter(progress -> ((Number) progress.get("userId")).longValue() == member.getId()) + .findFirst() + .orElseThrow(); + assertThat(targetMemberProgress.get("current")).isEqualTo(2); + assertThat(targetMemberProgress.get("cleared")).isEqualTo(true); + } + + @Test + void nonMemberCannotGetChallenges() throws Exception { + authenticate(outsider.getId()); + + mockMvc.perform(get("/api/study-room/{roomId}/challenges", roomId)) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(10002)); + } + + @Test + void createChallengeRejectsInvalidPeriod() throws Exception { + ChallengeCreateRequest request = new ChallengeCreateRequest( + "잘못된 기간", + "individual", + "practice_count", + "yearly", + 1, + null, + LocalDateTime.now().plusDays(1) + ); + + authenticate(member.getId()); + mockMvc.perform(post("/api/study-room/{roomId}/challenges", roomId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value(10016)); + } + + private void authenticate(Long userId) { + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken( + userId, + null, + List.of(new SimpleGrantedAuthority("ROLE_MEMBER")) + ) + ); + } + + private void createPractice(Long userId, LocalDateTime practicedAt) { + Problem problem = problemRepository.save(Problem.from( + new ProblemRegisterDto(null, "memo", "reference", null, LocalDateTime.now()), + userId + )); + problemSolveRepository.save(ProblemSolve.create( + problem, + userId, + practicedAt, + AnswerStatus.CORRECT, + null, + null, + 60 + )); + } +} From e37c98cfa3fee41ce82781e64ab486926fb00077 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sun, 28 Jun 2026 16:46:22 +0900 Subject: [PATCH 30/67] =?UTF-8?q?[Feat]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20=EC=98=A4=EB=8A=98=20=EB=B3=B5?= =?UTF-8?q?=EC=8A=B5=20=EC=97=AC=EB=B6=80=20=EC=9D=91=EB=8B=B5=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StudyRoomController.java | 6 +++ .../backend/studyroom/dto/StudyRoomDtos.java | 5 ++- .../backend/studyroom/entity/StudyRoom.java | 4 ++ .../studyroom/service/StudyRoomMapper.java | 3 +- .../studyroom/service/StudyRoomService.java | 11 +++++- .../StudyRoomApiIntegrationTest.java | 39 ++++++++++++++++++- 6 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java index e1ca040d..7548489c 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java @@ -37,6 +37,12 @@ public CommonResponse getRoom(@PathVariable("roomId") L return CommonResponse.success(studyRoomService.getRoom(roomId, currentUserId())); } + @PatchMapping("/{roomId}") + public CommonResponse updateRoom(@PathVariable("roomId") Long roomId, + @RequestBody StudyRoomUpdateRequest request) { + return CommonResponse.success(studyRoomService.updateRoom(roomId, currentUserId(), request)); + } + @DeleteMapping("/{roomId}") public CommonResponse deleteRoom(@PathVariable("roomId") Long roomId) { studyRoomService.deleteRoom(roomId, currentUserId()); diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java index 9e05c598..a5a4d774 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java @@ -13,6 +13,9 @@ private StudyRoomDtos() { public record StudyRoomCreateRequest(String name) { } + public record StudyRoomUpdateRequest(String name) { + } + public record StudyRoomJoinRequest(String code) { } @@ -41,7 +44,7 @@ public record StudyRoomDetailResponse(Long roomId, String name, Long hostUserId, public record StudyRoomMemberResponse(Long userId, String name, Long totalStudyLevel, int currentStreak, int weeklyProblemCount, int weeklyPracticeCount, Integer weeklyGoal, Integer goalProgress, - int todayPracticeCount) { + int todayPracticeCount, boolean practicedToday) { } public record StudyRoomThumbnailUpdateResponse(String thumbnailUrl) { diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoom.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoom.java index 6a75cf15..a9c808cf 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoom.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoom.java @@ -54,6 +54,10 @@ public void updateHostUserId(Long hostUserId) { this.hostUserId = hostUserId; } + public void updateName(String name) { + this.name = name; + } + public void updateThumbnailUrl(String thumbnailUrl) { this.thumbnailUrl = thumbnailUrl; } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomMapper.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomMapper.java index 2852236b..da6f0a7e 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomMapper.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomMapper.java @@ -70,7 +70,8 @@ public StudyRoomMemberResponse toMemberResponse(StudyRoomMember member, StudyRoo weeklyPracticeCount, member.getWeeklyGoal(), goalProgress, - todayPracticeCount + todayPracticeCount, + todayPracticeCount > 0 ); } } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java index c23a9304..d09ad36b 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java @@ -68,7 +68,7 @@ public StudyRoomDetailResponse getRoom(Long roomId, Long userId) { @Transactional public StudyRoomDetailResponse createRoom(StudyRoomCreateRequest request, Long userId) { - String name = validateName(request.name()); + String name = validateName(request == null ? null : request.name()); User user = userRepository.findByIdForUpdate(userId) .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); if (memberRepository.countByUserId(userId) >= MAX_USER_ROOM_COUNT) { @@ -80,6 +80,15 @@ public StudyRoomDetailResponse createRoom(StudyRoomCreateRequest request, Long u return buildDetail(room); } + @Transactional + public StudyRoomDetailResponse updateRoom(Long roomId, Long userId, StudyRoomUpdateRequest request) { + String name = validateName(request == null ? null : request.name()); + accessService.validateHost(roomId, userId); + StudyRoom room = lockRoom(roomId); + room.updateName(name); + return buildDetail(room); + } + @Transactional public void deleteRoom(Long roomId, Long userId) { accessService.validateHost(roomId, userId); diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java index 89406e43..f1bb59e1 100644 --- a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java @@ -100,7 +100,9 @@ void roomCreateInviteJoinReadAndTodayPracticeSummaryApiFlow() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.data.memberCount").value(2)) .andExpect(jsonPath("$.data.members[?(@.userId == " + host.getId() + ")].todayPracticeCount").value(2)) - .andExpect(jsonPath("$.data.members[?(@.userId == " + member.getId() + ")].todayPracticeCount").value(3)); + .andExpect(jsonPath("$.data.members[?(@.userId == " + host.getId() + ")].practicedToday").value(true)) + .andExpect(jsonPath("$.data.members[?(@.userId == " + member.getId() + ")].todayPracticeCount").value(3)) + .andExpect(jsonPath("$.data.members[?(@.userId == " + member.getId() + ")].practicedToday").value(true)); mockMvc.perform(get("/api/study-room") .with(auth())) @@ -109,6 +111,41 @@ void roomCreateInviteJoinReadAndTodayPracticeSummaryApiFlow() throws Exception { .andExpect(jsonPath("$.data[?(@.roomId == " + roomId + ")].todayPracticeCount").value(5)); } + @Test + void hostCanUpdateRoomNameAndMemberCannotUpdateRoomName() throws Exception { + User host = saveUser("방장", "room-update-host"); + User member = saveUser("멤버", "room-update-member"); + authenticate(host.getId()); + Long roomId = createRoom("기존 이름"); + String inviteCode = issueInviteCode(roomId); + authenticate(member.getId()); + join(inviteCode); + + mockMvc.perform(patch("/api/study-room/{roomId}", roomId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomUpdateRequest("멤버 수정")))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(10003)); + + authenticate(host.getId()); + mockMvc.perform(patch("/api/study-room/{roomId}", roomId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomUpdateRequest(" 새 스터디룸 ")))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.roomId").value(roomId)) + .andExpect(jsonPath("$.data.name").value("새 스터디룸")) + .andExpect(jsonPath("$.data.memberCount").value(2)); + + mockMvc.perform(patch("/api/study-room/{roomId}", roomId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomUpdateRequest(" ")))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value(10016)); + } + @Test void nonMemberCannotReadRoomAndMemberCannotKickOtherMember() throws Exception { User host = saveUser("방장", "permission-host"); From d8e8048a6c61d12e290f74960dc63aae5a95e71c Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sun, 28 Jun 2026 18:28:01 +0900 Subject: [PATCH 31/67] =?UTF-8?q?[Feat]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EA=B2=80=EC=A6=9D=20=EB=B3=B4=EA=B0=95=20-=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20MIME=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=A0=95=EB=A6=AC=20=EC=A0=95=EC=B1=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyroom/service/StudyRoomService.java | 121 +++++++++++++++++- .../exception/FileUploadErrorCase.java | 6 +- .../StudyRoomApiIntegrationTest.java | 67 ++++++++++ .../StudyRoomServiceIntegrationTest.java | 49 ++++++- 4 files changed, 233 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java index d09ad36b..bc8dbd16 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java @@ -19,9 +19,13 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.util.List; +import java.util.Locale; import java.util.Map; @Service @@ -30,6 +34,7 @@ public class StudyRoomService { public static final int MAX_ROOM_MEMBER_COUNT = 20; public static final int MAX_USER_ROOM_COUNT = 10; + private static final long MAX_THUMBNAIL_SIZE_BYTES = 5 * 1024 * 1024; private final StudyRoomRepository roomRepository; private final StudyRoomMemberRepository memberRepository; @@ -93,6 +98,7 @@ public StudyRoomDetailResponse updateRoom(Long roomId, Long userId, StudyRoomUpd public void deleteRoom(Long roomId, Long userId) { accessService.validateHost(roomId, userId); StudyRoom room = lockRoom(roomId); + deleteThumbnailAsync(room.getThumbnailUrl(), roomId); roomRepository.delete(room); } @@ -118,6 +124,7 @@ public void leaveRoom(Long roomId, Long userId) { .findFirst() .orElse(null); if (nextHost == null) { + deleteThumbnailAsync(room.getThumbnailUrl(), roomId); roomRepository.delete(room); return; } @@ -151,16 +158,13 @@ public GoalUpdateResponse updateGoal(Long roomId, Long userId, StudyRoomGoalUpda @Transactional public StudyRoomThumbnailUpdateResponse updateThumbnail(Long roomId, Long userId, MultipartFile thumbnail) { accessService.validateHost(roomId, userId); - if (thumbnail == null || thumbnail.isEmpty()) { - throw new ApplicationException(FileUploadErrorCase.FILE_UPLOAD_FAILED); - } + validateThumbnail(thumbnail); StudyRoom room = accessService.getRoomOrThrow(roomId); String previousThumbnailUrl = room.getThumbnailUrl(); String thumbnailUrl = fileUploadService.uploadFileToS3(thumbnail); + deleteThumbnailOnRollback(thumbnailUrl, roomId); room.updateThumbnailUrl(thumbnailUrl); - if (previousThumbnailUrl != null && !previousThumbnailUrl.isBlank()) { - s3DeleteProducer.sendDeleteMessage(previousThumbnailUrl, roomId); - } + deleteThumbnailAsync(previousThumbnailUrl, roomId); return new StudyRoomThumbnailUpdateResponse(thumbnailUrl); } @@ -178,4 +182,109 @@ private String validateName(String name) { } return name.trim(); } + + private void validateThumbnail(MultipartFile thumbnail) { + if (thumbnail == null || thumbnail.isEmpty()) { + throw new ApplicationException(FileUploadErrorCase.INVALID_IMAGE_FILE); + } + if (thumbnail.getSize() > MAX_THUMBNAIL_SIZE_BYTES) { + throw new ApplicationException(FileUploadErrorCase.FILE_SIZE_EXCEEDED); + } + + String extension = getLowercaseExtension(thumbnail.getOriginalFilename()); + String contentType = thumbnail.getContentType(); + if (!isAllowedImageType(contentType, extension) || !hasAllowedImageSignature(thumbnail, contentType)) { + throw new ApplicationException(FileUploadErrorCase.INVALID_IMAGE_FILE); + } + } + + private String getLowercaseExtension(String originalFilename) { + if (originalFilename == null) { + throw new ApplicationException(FileUploadErrorCase.INVALID_IMAGE_FILE); + } + int dotIndex = originalFilename.lastIndexOf('.'); + if (dotIndex < 0 || dotIndex == originalFilename.length() - 1) { + throw new ApplicationException(FileUploadErrorCase.INVALID_IMAGE_FILE); + } + return originalFilename.substring(dotIndex + 1).toLowerCase(Locale.ROOT); + } + + private boolean isAllowedImageType(String contentType, String extension) { + if (contentType == null) { + return false; + } + return switch (contentType) { + case "image/jpeg" -> extension.equals("jpg") || extension.equals("jpeg"); + case "image/png" -> extension.equals("png"); + case "image/webp" -> extension.equals("webp"); + default -> false; + }; + } + + private boolean hasAllowedImageSignature(MultipartFile thumbnail, String contentType) { + byte[] header = new byte[12]; + int read; + try { + read = thumbnail.getInputStream().read(header); + } catch (IOException e) { + throw new ApplicationException(FileUploadErrorCase.FILE_UPLOAD_FAILED); + } + + return switch (contentType) { + case "image/jpeg" -> read >= 3 + && (header[0] & 0xFF) == 0xFF + && (header[1] & 0xFF) == 0xD8 + && (header[2] & 0xFF) == 0xFF; + case "image/png" -> read >= 8 + && (header[0] & 0xFF) == 0x89 + && header[1] == 0x50 + && header[2] == 0x4E + && header[3] == 0x47 + && header[4] == 0x0D + && header[5] == 0x0A + && header[6] == 0x1A + && header[7] == 0x0A; + case "image/webp" -> read >= 12 + && header[0] == 0x52 + && header[1] == 0x49 + && header[2] == 0x46 + && header[3] == 0x46 + && header[8] == 0x57 + && header[9] == 0x45 + && header[10] == 0x42 + && header[11] == 0x50; + default -> false; + }; + } + + private void deleteThumbnailAsync(String thumbnailUrl, Long roomId) { + if (thumbnailUrl == null || thumbnailUrl.isBlank()) { + return; + } + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + s3DeleteProducer.sendDeleteMessage(thumbnailUrl, roomId); + return; + } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + s3DeleteProducer.sendDeleteMessage(thumbnailUrl, roomId); + } + }); + } + + private void deleteThumbnailOnRollback(String thumbnailUrl, Long roomId) { + if (thumbnailUrl == null || thumbnailUrl.isBlank() + || !TransactionSynchronizationManager.isSynchronizationActive()) { + return; + } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCompletion(int status) { + if (status == STATUS_ROLLED_BACK) { + s3DeleteProducer.sendDeleteMessage(thumbnailUrl, roomId); + } + } + }); + } } diff --git a/src/main/java/com/aisip/OnO/backend/util/fileupload/exception/FileUploadErrorCase.java b/src/main/java/com/aisip/OnO/backend/util/fileupload/exception/FileUploadErrorCase.java index d0857280..fea6a326 100644 --- a/src/main/java/com/aisip/OnO/backend/util/fileupload/exception/FileUploadErrorCase.java +++ b/src/main/java/com/aisip/OnO/backend/util/fileupload/exception/FileUploadErrorCase.java @@ -10,7 +10,11 @@ public enum FileUploadErrorCase implements ErrorCase { FILE_UPLOAD_FAILED(400, 2001, "파일 업로드 중 문제가 발생했습니다."), - FILE_NOT_FOUND(404, 2002, "파일을 찾을 수 없습니다."); + FILE_NOT_FOUND(404, 2002, "파일을 찾을 수 없습니다."), + + INVALID_IMAGE_FILE(400, 2003, "이미지 파일 형식이 올바르지 않습니다."), + + FILE_SIZE_EXCEEDED(400, 2004, "파일 최대 용량을 초과했습니다."); private final Integer httpStatusCode; private final Integer errorCode; diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java index f1bb59e1..87a2308d 100644 --- a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java @@ -15,6 +15,8 @@ import com.aisip.OnO.backend.user.entity.User; import com.aisip.OnO.backend.user.repository.UserRepository; import com.aisip.OnO.backend.util.RandomUserGenerator; +import com.aisip.OnO.backend.util.fileupload.service.FileUploadService; +import com.aisip.OnO.backend.config.rabbitmq.producer.S3DeleteProducer; import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; import org.junit.jupiter.api.AfterEach; @@ -22,7 +24,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; @@ -35,6 +39,8 @@ import java.time.temporal.ChronoUnit; import java.util.List; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -68,6 +74,12 @@ class StudyRoomApiIntegrationTest { @Autowired private ProblemSolveRepository problemSolveRepository; + @MockBean + private FileUploadService fileUploadService; + + @MockBean + private S3DeleteProducer s3DeleteProducer; + private Long authenticatedUserId; @AfterEach @@ -146,6 +158,53 @@ void hostCanUpdateRoomNameAndMemberCannotUpdateRoomName() throws Exception { .andExpect(jsonPath("$.errorCode").value(10016)); } + @Test + void hostCanUpdateThumbnailAndResponsesIncludeThumbnailUrl() throws Exception { + User host = saveUser("방장", "thumbnail-host"); + authenticate(host.getId()); + Long roomId = createRoom("썸네일방"); + MockMultipartFile thumbnail = new MockMultipartFile("thumbnail", "thumbnail.png", "image/png", pngBytes()); + given(fileUploadService.uploadFileToS3(any())).willReturn("https://cdn.example.com/study-room.png"); + + mockMvc.perform(multipart("/api/study-room/{roomId}/thumbnail", roomId) + .file(thumbnail) + .with(request -> { + request.setMethod("PATCH"); + return request; + }) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.thumbnailUrl").value("https://cdn.example.com/study-room.png")); + + mockMvc.perform(get("/api/study-room/{roomId}", roomId) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.thumbnailUrl").value("https://cdn.example.com/study-room.png")); + + mockMvc.perform(get("/api/study-room") + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data[?(@.roomId == " + roomId + ")].thumbnailUrl").value("https://cdn.example.com/study-room.png")); + } + + @Test + void thumbnailUploadRejectsInvalidImageFile() throws Exception { + User host = saveUser("방장", "thumbnail-invalid-host"); + authenticate(host.getId()); + Long roomId = createRoom("썸네일 검증방"); + MockMultipartFile thumbnail = new MockMultipartFile("thumbnail", "thumbnail.png", "image/png", "not-image".getBytes()); + + mockMvc.perform(multipart("/api/study-room/{roomId}/thumbnail", roomId) + .file(thumbnail) + .with(request -> { + request.setMethod("PATCH"); + return request; + }) + .with(auth())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value(2003)); + } + @Test void nonMemberCannotReadRoomAndMemberCannotKickOtherMember() throws Exception { User host = saveUser("방장", "permission-host"); @@ -410,4 +469,12 @@ private void createPractice(Long userId, LocalDateTime practicedAt, int count) { )); } } + + private byte[] pngBytes() { + return new byte[]{ + (byte) 0x89, 0x50, 0x4E, 0x47, + 0x0D, 0x0A, 0x1A, 0x0A, + 0x00, 0x00, 0x00, 0x0D + }; + } } diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java index 835207b6..290a11f6 100644 --- a/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java +++ b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java @@ -26,6 +26,7 @@ import com.aisip.OnO.backend.user.entity.User; import com.aisip.OnO.backend.user.repository.UserRepository; import com.aisip.OnO.backend.util.RandomUserGenerator; +import com.aisip.OnO.backend.util.fileupload.exception.FileUploadErrorCase; import com.aisip.OnO.backend.util.fileupload.service.FileUploadService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -39,6 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @SpringBootTest @@ -196,8 +198,8 @@ void getRoomIncludesMemberTodayPracticeCount() { void updateThumbnailUploadsAndStoresThumbnailUrl() { User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); StudyRoomDetailResponse room = studyRoomService.createRoom(new StudyRoomCreateRequest("수능 준비방"), host.getId()); - MockMultipartFile firstThumbnail = new MockMultipartFile("thumbnail", "first.png", "image/png", "first".getBytes()); - MockMultipartFile secondThumbnail = new MockMultipartFile("thumbnail", "second.png", "image/png", "second".getBytes()); + MockMultipartFile firstThumbnail = new MockMultipartFile("thumbnail", "first.png", "image/png", pngBytes()); + MockMultipartFile secondThumbnail = new MockMultipartFile("thumbnail", "second.png", "image/png", pngBytes()); given(fileUploadService.uploadFileToS3(firstThumbnail)).willReturn("https://cdn.example.com/first.png"); given(fileUploadService.uploadFileToS3(secondThumbnail)).willReturn("https://cdn.example.com/second.png"); @@ -218,7 +220,7 @@ void nonHostCannotUpdateThumbnail() { StudyRoomDetailResponse room = studyRoomService.createRoom(new StudyRoomCreateRequest("수능 준비방"), host.getId()); InviteCodeResponse invite = inviteService.issueInviteCode(room.roomId(), host.getId()); inviteService.join(new StudyRoomJoinRequest(invite.code()), member.getId()); - MockMultipartFile thumbnail = new MockMultipartFile("thumbnail", "first.png", "image/png", "first".getBytes()); + MockMultipartFile thumbnail = new MockMultipartFile("thumbnail", "first.png", "image/png", pngBytes()); assertThatThrownBy(() -> studyRoomService.updateThumbnail(room.roomId(), member.getId(), thumbnail)) .isInstanceOf(ApplicationException.class) @@ -226,6 +228,32 @@ void nonHostCannotUpdateThumbnail() { .isEqualTo(StudyRoomErrorCase.STUDY_ROOM_HOST_ONLY); } + @Test + void updateThumbnailRejectsInvalidMimeAndSignature() { + User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); + StudyRoomDetailResponse room = studyRoomService.createRoom(new StudyRoomCreateRequest("수능 준비방"), host.getId()); + MockMultipartFile thumbnail = new MockMultipartFile("thumbnail", "first.png", "image/png", "not-image".getBytes()); + + assertThatThrownBy(() -> studyRoomService.updateThumbnail(room.roomId(), host.getId(), thumbnail)) + .isInstanceOf(ApplicationException.class) + .extracting("errorCase") + .isEqualTo(FileUploadErrorCase.INVALID_IMAGE_FILE); + verify(fileUploadService, never()).uploadFileToS3(thumbnail); + } + + @Test + void updateThumbnailRejectsOversizedImage() { + User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); + StudyRoomDetailResponse room = studyRoomService.createRoom(new StudyRoomCreateRequest("수능 준비방"), host.getId()); + MockMultipartFile thumbnail = new MockMultipartFile("thumbnail", "large.png", "image/png", largePngBytes()); + + assertThatThrownBy(() -> studyRoomService.updateThumbnail(room.roomId(), host.getId(), thumbnail)) + .isInstanceOf(ApplicationException.class) + .extracting("errorCase") + .isEqualTo(FileUploadErrorCase.FILE_SIZE_EXCEEDED); + verify(fileUploadService, never()).uploadFileToS3(thumbnail); + } + @Test void hostLeaveTransfersHostToNextMember() { User host = userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", "방장", "host")); @@ -280,4 +308,19 @@ private void createPractice(Long userId, int count) { )); } } + + private byte[] pngBytes() { + return new byte[]{ + (byte) 0x89, 0x50, 0x4E, 0x47, + 0x0D, 0x0A, 0x1A, 0x0A, + 0x00, 0x00, 0x00, 0x0D + }; + } + + private byte[] largePngBytes() { + byte[] bytes = new byte[(5 * 1024 * 1024) + 1]; + byte[] header = pngBytes(); + System.arraycopy(header, 0, bytes, 0, header.length); + return bytes; + } } From b397629e36ea7782df298862c391da3f5c1d2b86 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sun, 28 Jun 2026 23:13:29 +0900 Subject: [PATCH 32/67] =?UTF-8?q?[Feat]=20=EA=B3=B5=EC=9C=A0=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EB=8C=93=EA=B8=80=20API=20=EC=B6=94=EA=B0=80=20-?= =?UTF-8?q?=20=EB=8C=93=EA=B8=80=20CRUD=20=ED=86=B5=ED=95=A9=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20API=20=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StudyRoomController.java | 11 +- .../StudyRoomSharedProblemController.java | 36 ++- .../backend/studyroom/dto/StudyRoomDtos.java | 8 + .../entity/StudyRoomSharedProblemComment.java | 45 +++ .../exception/StudyRoomErrorCase.java | 3 +- ...udyRoomSharedProblemCommentRepository.java | 41 +++ .../studyroom/service/StudyRoomService.java | 21 ++ .../StudyRoomSharedProblemCommentService.java | 123 ++++++++ .../StudyRoomSharedProblemService.java | 2 + .../V10__add_shared_problem_comment.sql | 14 + .../StudyRoomApiIntegrationTest.java | 22 ++ .../StudyRoomSharedProblemCommentApiTest.java | 268 ++++++++++++++++++ 12 files changed, 590 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblemComment.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemCommentRepository.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemCommentService.java create mode 100644 src/main/resources/db/migration/V10__add_shared_problem_comment.sql create mode 100644 src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemCommentApiTest.java diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java index 7548489c..85e26c4a 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java @@ -15,7 +15,7 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/study-room") +@RequestMapping({"/api/study-room", "/api/study-rooms"}) public class StudyRoomController { private final StudyRoomService studyRoomService; @@ -37,12 +37,19 @@ public CommonResponse getRoom(@PathVariable("roomId") L return CommonResponse.success(studyRoomService.getRoom(roomId, currentUserId())); } - @PatchMapping("/{roomId}") + @PatchMapping(value = "/{roomId}", consumes = MediaType.APPLICATION_JSON_VALUE) public CommonResponse updateRoom(@PathVariable("roomId") Long roomId, @RequestBody StudyRoomUpdateRequest request) { return CommonResponse.success(studyRoomService.updateRoom(roomId, currentUserId(), request)); } + @PatchMapping(value = "/{roomId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public CommonResponse updateRoomMultipart(@PathVariable("roomId") Long roomId, + @RequestParam(value = "name", required = false) String name, + @RequestPart(value = "thumbnailImage", required = false) MultipartFile thumbnailImage) { + return CommonResponse.success(studyRoomService.updateRoom(roomId, currentUserId(), name, thumbnailImage)); + } + @DeleteMapping("/{roomId}") public CommonResponse deleteRoom(@PathVariable("roomId") Long roomId) { studyRoomService.deleteRoom(roomId, currentUserId()); diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomSharedProblemController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomSharedProblemController.java index 5b2c7d42..4dcba9b4 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomSharedProblemController.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomSharedProblemController.java @@ -3,6 +3,7 @@ import com.aisip.OnO.backend.common.response.CommonResponse; import com.aisip.OnO.backend.common.response.CursorPageResponse; import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.studyroom.service.StudyRoomSharedProblemCommentService; import com.aisip.OnO.backend.studyroom.service.StudyRoomSharedProblemService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -11,10 +12,11 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/study-room/{roomId}/shared-problems") +@RequestMapping({"/api/study-room/{roomId}/shared-problems", "/api/study-rooms/{roomId}/shared-problems"}) public class StudyRoomSharedProblemController { private final StudyRoomSharedProblemService sharedProblemService; + private final StudyRoomSharedProblemCommentService commentService; @GetMapping public CommonResponse> getSharedProblems(@PathVariable("roomId") Long roomId, @@ -37,6 +39,38 @@ public CommonResponse toggleReaction(@PathV return CommonResponse.success(sharedProblemService.toggleReaction(roomId, sharedProblemId, currentUserId(), request)); } + @GetMapping("/{sharedProblemId}/comments") + public CommonResponse> getComments(@PathVariable("roomId") Long roomId, + @PathVariable("sharedProblemId") Long sharedProblemId, + @RequestParam(value = "cursor", required = false) Long cursor, + @RequestParam(value = "size", defaultValue = "20") int size) { + return CommonResponse.success(commentService.getComments(roomId, sharedProblemId, currentUserId(), cursor, size)); + } + + @PostMapping("/{sharedProblemId}/comments") + @ResponseStatus(HttpStatus.CREATED) + public CommonResponse createComment(@PathVariable("roomId") Long roomId, + @PathVariable("sharedProblemId") Long sharedProblemId, + @RequestBody SharedProblemCommentRequest request) { + return CommonResponse.success(commentService.createComment(roomId, sharedProblemId, currentUserId(), request)); + } + + @PatchMapping("/{sharedProblemId}/comments/{commentId}") + public CommonResponse updateComment(@PathVariable("roomId") Long roomId, + @PathVariable("sharedProblemId") Long sharedProblemId, + @PathVariable("commentId") Long commentId, + @RequestBody SharedProblemCommentRequest request) { + return CommonResponse.success(commentService.updateComment(roomId, sharedProblemId, commentId, currentUserId(), request)); + } + + @DeleteMapping("/{sharedProblemId}/comments/{commentId}") + public CommonResponse deleteComment(@PathVariable("roomId") Long roomId, + @PathVariable("sharedProblemId") Long sharedProblemId, + @PathVariable("commentId") Long commentId) { + commentService.deleteComment(roomId, sharedProblemId, commentId, currentUserId()); + return CommonResponse.success("공유 문제 댓글 삭제가 완료되었습니다."); + } + @DeleteMapping("/{sharedProblemId}") public CommonResponse deleteSharedProblem(@PathVariable("roomId") Long roomId, @PathVariable("sharedProblemId") Long sharedProblemId) { diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java index a5a4d774..38c15eae 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java @@ -32,6 +32,9 @@ public record ChallengeCreateRequest(String title, String type, String metric, S public record SharedProblemCreateRequest(Long problemId, String comment) { } + public record SharedProblemCommentRequest(String content) { + } + public record StudyRoomListResponse(Long roomId, String name, Long hostUserId, int memberCount, String thumbnailUrl, boolean hasUnreadReport, int todayPracticeMemberCount, int todayPracticeCount) { @@ -101,6 +104,11 @@ public record SharedProblemResponse(Long sharedProblemId, Long sharedByUserId, S public record SharedProblemReactionToggleResponse(Long sharedProblemId, List reactions) { } + public record SharedProblemCommentResponse(Long commentId, String content, Long authorId, String authorName, + String authorProfileImageUrl, LocalDateTime createdAt, + LocalDateTime updatedAt, boolean isEdited, boolean isMine) { + } + public record WeeklyReportResponse(Long reportId, LocalDate weekStart, LocalDate weekEnd, String topMemberName, Integer topMemberProblemCount, String longestStreakName, Integer longestStreakDays, diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblemComment.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblemComment.java new file mode 100644 index 00000000..14707564 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblemComment.java @@ -0,0 +1,45 @@ +package com.aisip.OnO.backend.studyroom.entity; + +import com.aisip.OnO.backend.common.entity.BaseEntity; +import com.aisip.OnO.backend.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +@Getter +@Entity +@Builder(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "study_room_shared_problem_comment", indexes = { + @Index(name = "idx_shared_problem_comment_problem_id", columnList = "shared_problem_id, id"), + @Index(name = "idx_shared_problem_comment_author", columnList = "author_id") +}) +public class StudyRoomSharedProblemComment extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "shared_problem_id", nullable = false) + private StudyRoomSharedProblem sharedProblem; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "author_id", nullable = false) + private User author; + + @Column(nullable = false, length = 1000) + private String content; + + public static StudyRoomSharedProblemComment create(StudyRoomSharedProblem sharedProblem, User author, String content) { + return StudyRoomSharedProblemComment.builder() + .sharedProblem(sharedProblem) + .author(author) + .content(content) + .build(); + } + + public void updateContent(String content) { + this.content = content; + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/exception/StudyRoomErrorCase.java b/src/main/java/com/aisip/OnO/backend/studyroom/exception/StudyRoomErrorCase.java index 0b95aec6..1aa93e76 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/exception/StudyRoomErrorCase.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/exception/StudyRoomErrorCase.java @@ -23,7 +23,8 @@ public enum StudyRoomErrorCase implements ErrorCase { SHARED_PROBLEM_NOT_FOUND(404, 10013, "공유 문제를 찾을 수 없습니다."), REPORT_NOT_FOUND(404, 10014, "주간 리포트를 찾을 수 없습니다."), INVALID_REACTION_EMOJI(400, 10015, "허용되지 않은 반응입니다."), - INVALID_STUDY_ROOM_REQUEST(400, 10016, "스터디룸 요청 값이 올바르지 않습니다."); + INVALID_STUDY_ROOM_REQUEST(400, 10016, "스터디룸 요청 값이 올바르지 않습니다."), + SHARED_PROBLEM_COMMENT_NOT_FOUND(404, 10017, "공유 문제 댓글을 찾을 수 없습니다."); private final Integer httpStatusCode; private final Integer errorCode; diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemCommentRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemCommentRepository.java new file mode 100644 index 00000000..5526e5db --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemCommentRepository.java @@ -0,0 +1,41 @@ +package com.aisip.OnO.backend.studyroom.repository; + +import com.aisip.OnO.backend.studyroom.entity.StudyRoomSharedProblemComment; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface StudyRoomSharedProblemCommentRepository extends JpaRepository { + + @Query(""" + select c + from StudyRoomSharedProblemComment c + join fetch c.author + where c.sharedProblem.id = :sharedProblemId + and (:cursor is null or c.id < :cursor) + order by c.id desc + """) + List findBySharedProblemIdAndCursor(@Param("sharedProblemId") Long sharedProblemId, + @Param("cursor") Long cursor, + Pageable pageable); + + @Query(""" + select c + from StudyRoomSharedProblemComment c + join fetch c.author + join fetch c.sharedProblem sp + join fetch sp.room + where c.id = :commentId + and sp.id = :sharedProblemId + and sp.room.id = :roomId + """) + Optional findByIdAndSharedProblemIdAndRoomId(@Param("commentId") Long commentId, + @Param("sharedProblemId") Long sharedProblemId, + @Param("roomId") Long roomId); + + void deleteBySharedProblemId(Long sharedProblemId); +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java index bc8dbd16..f20d1fce 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java @@ -94,6 +94,27 @@ public StudyRoomDetailResponse updateRoom(Long roomId, Long userId, StudyRoomUpd return buildDetail(room); } + @Transactional + public StudyRoomDetailResponse updateRoom(Long roomId, Long userId, String name, MultipartFile thumbnailImage) { + if ((name == null || name.isBlank()) && (thumbnailImage == null || thumbnailImage.isEmpty())) { + throw new ApplicationException(StudyRoomErrorCase.INVALID_STUDY_ROOM_REQUEST); + } + accessService.validateHost(roomId, userId); + StudyRoom room = lockRoom(roomId); + if (name != null) { + room.updateName(validateName(name)); + } + if (thumbnailImage != null) { + validateThumbnail(thumbnailImage); + String previousThumbnailUrl = room.getThumbnailUrl(); + String thumbnailUrl = fileUploadService.uploadFileToS3(thumbnailImage); + deleteThumbnailOnRollback(thumbnailUrl, roomId); + room.updateThumbnailUrl(thumbnailUrl); + deleteThumbnailAsync(previousThumbnailUrl, roomId); + } + return buildDetail(room); + } + @Transactional public void deleteRoom(Long roomId, Long userId) { accessService.validateHost(roomId, userId); diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemCommentService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemCommentService.java new file mode 100644 index 00000000..e812b860 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemCommentService.java @@ -0,0 +1,123 @@ +package com.aisip.OnO.backend.studyroom.service; + +import com.aisip.OnO.backend.common.exception.ApplicationException; +import com.aisip.OnO.backend.common.response.CursorPageResponse; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomMember; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomMemberRole; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomSharedProblem; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomSharedProblemComment; +import com.aisip.OnO.backend.studyroom.exception.StudyRoomErrorCase; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomSharedProblemCommentRepository; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomSharedProblemRepository; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.exception.UserErrorCase; +import com.aisip.OnO.backend.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class StudyRoomSharedProblemCommentService { + + private static final int MAX_COMMENT_LENGTH = 1000; + + private final StudyRoomAccessService accessService; + private final StudyRoomSharedProblemRepository sharedProblemRepository; + private final StudyRoomSharedProblemCommentRepository commentRepository; + private final UserRepository userRepository; + + @Transactional(readOnly = true) + public CursorPageResponse getComments(Long roomId, Long sharedProblemId, + Long userId, Long cursor, int size) { + accessService.validateMember(roomId, userId); + getSharedProblemOrThrow(roomId, sharedProblemId); + int safeSize = Math.min(Math.max(size, 1), 50); + List comments = commentRepository.findBySharedProblemIdAndCursor( + sharedProblemId, cursor, PageRequest.of(0, safeSize + 1)); + boolean hasNext = comments.size() > safeSize; + List contentComments = hasNext ? comments.subList(0, safeSize) : comments; + List content = contentComments.stream() + .map(comment -> toResponse(comment, userId)) + .toList(); + Long nextCursor = hasNext && !contentComments.isEmpty() + ? contentComments.get(contentComments.size() - 1).getId() + : null; + return new CursorPageResponse<>(content, nextCursor, hasNext, safeSize); + } + + @Transactional + public SharedProblemCommentResponse createComment(Long roomId, Long sharedProblemId, Long userId, + SharedProblemCommentRequest request) { + accessService.validateMember(roomId, userId); + StudyRoomSharedProblem sharedProblem = getSharedProblemOrThrow(roomId, sharedProblemId); + User user = userRepository.findById(userId) + .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); + StudyRoomSharedProblemComment comment = commentRepository.save( + StudyRoomSharedProblemComment.create(sharedProblem, user, validateContent(request))); + return toResponse(comment, userId); + } + + @Transactional + public SharedProblemCommentResponse updateComment(Long roomId, Long sharedProblemId, Long commentId, + Long userId, SharedProblemCommentRequest request) { + accessService.validateMember(roomId, userId); + StudyRoomSharedProblemComment comment = getCommentOrThrow(roomId, sharedProblemId, commentId); + if (!comment.getAuthor().getId().equals(userId)) { + throw new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_FORBIDDEN); + } + comment.updateContent(validateContent(request)); + return toResponse(comment, userId); + } + + @Transactional + public void deleteComment(Long roomId, Long sharedProblemId, Long commentId, Long userId) { + StudyRoomMember member = accessService.getMemberOrThrow(roomId, userId); + StudyRoomSharedProblemComment comment = getCommentOrThrow(roomId, sharedProblemId, commentId); + boolean mine = comment.getAuthor().getId().equals(userId); + boolean host = member.getRole() == StudyRoomMemberRole.HOST; + if (!mine && !host) { + throw new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_FORBIDDEN); + } + commentRepository.delete(comment); + } + + private StudyRoomSharedProblem getSharedProblemOrThrow(Long roomId, Long sharedProblemId) { + return sharedProblemRepository.findByIdAndRoomId(sharedProblemId, roomId) + .orElseThrow(() -> new ApplicationException(StudyRoomErrorCase.SHARED_PROBLEM_NOT_FOUND)); + } + + private StudyRoomSharedProblemComment getCommentOrThrow(Long roomId, Long sharedProblemId, Long commentId) { + return commentRepository.findByIdAndSharedProblemIdAndRoomId(commentId, sharedProblemId, roomId) + .orElseThrow(() -> new ApplicationException(StudyRoomErrorCase.SHARED_PROBLEM_COMMENT_NOT_FOUND)); + } + + private String validateContent(SharedProblemCommentRequest request) { + String content = request == null ? null : request.content(); + if (content == null || content.isBlank() || content.length() > MAX_COMMENT_LENGTH) { + throw new ApplicationException(StudyRoomErrorCase.INVALID_STUDY_ROOM_REQUEST); + } + return content.trim(); + } + + private SharedProblemCommentResponse toResponse(StudyRoomSharedProblemComment comment, Long userId) { + LocalDateTime createdAt = comment.getCreatedAt(); + LocalDateTime updatedAt = comment.getUpdatedAt(); + return new SharedProblemCommentResponse( + comment.getId(), + comment.getContent(), + comment.getAuthor().getId(), + comment.getAuthor().getName(), + null, + createdAt, + updatedAt, + createdAt != null && updatedAt != null && updatedAt.isAfter(createdAt), + comment.getAuthor().getId().equals(userId) + ); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java index 09b35012..2c3e75c8 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java @@ -32,6 +32,7 @@ public class StudyRoomSharedProblemService { private final StudyRoomAccessService accessService; private final StudyRoomSharedProblemRepository sharedProblemRepository; private final StudyRoomSharedProblemReactionRepository reactionRepository; + private final StudyRoomSharedProblemCommentRepository commentRepository; private final UserRepository userRepository; private final ProblemService problemService; private final StudyRoomReactionService reactionService; @@ -98,6 +99,7 @@ public void deleteSharedProblem(Long roomId, Long sharedProblemId, Long userId) if (!sharedProblem.getSharedByUser().getId().equals(userId)) { throw new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_FORBIDDEN); } + commentRepository.deleteBySharedProblemId(sharedProblemId); reactionRepository.deleteBySharedProblemId(sharedProblemId); sharedProblemRepository.delete(sharedProblem); } diff --git a/src/main/resources/db/migration/V10__add_shared_problem_comment.sql b/src/main/resources/db/migration/V10__add_shared_problem_comment.sql new file mode 100644 index 00000000..e7fb45b5 --- /dev/null +++ b/src/main/resources/db/migration/V10__add_shared_problem_comment.sql @@ -0,0 +1,14 @@ +CREATE TABLE study_room_shared_problem_comment ( + id BIGINT NOT NULL AUTO_INCREMENT, + shared_problem_id BIGINT NOT NULL, + author_id BIGINT NOT NULL, + content VARCHAR(1000) NOT NULL, + created_at DATETIME(6), + updated_at DATETIME(6), + deleted_at DATETIME(6), + PRIMARY KEY (id), + CONSTRAINT fk_shared_problem_comment_problem FOREIGN KEY (shared_problem_id) REFERENCES study_room_shared_problem (id) ON DELETE CASCADE, + CONSTRAINT fk_shared_problem_comment_author FOREIGN KEY (author_id) REFERENCES user (id) +); +CREATE INDEX idx_shared_problem_comment_problem_id ON study_room_shared_problem_comment (shared_problem_id, id); +CREATE INDEX idx_shared_problem_comment_author ON study_room_shared_problem_comment (author_id); diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java index 87a2308d..111c2fda 100644 --- a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java @@ -187,6 +187,28 @@ void hostCanUpdateThumbnailAndResponsesIncludeThumbnailUrl() throws Exception { .andExpect(jsonPath("$.data[?(@.roomId == " + roomId + ")].thumbnailUrl").value("https://cdn.example.com/study-room.png")); } + @Test + void hostCanUpdateRoomNameAndThumbnailInSingleMultipartRequest() throws Exception { + User host = saveUser("방장", "room-multipart-host"); + authenticate(host.getId()); + Long roomId = createRoom("기존 통합 수정방"); + MockMultipartFile thumbnailImage = new MockMultipartFile("thumbnailImage", "thumbnail.png", "image/png", pngBytes()); + given(fileUploadService.uploadFileToS3(any())).willReturn("https://cdn.example.com/updated-room.png"); + + mockMvc.perform(multipart("/api/study-rooms/{roomId}", roomId) + .file(thumbnailImage) + .param("name", "통합 수정방") + .with(request -> { + request.setMethod("PATCH"); + return request; + }) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.roomId").value(roomId)) + .andExpect(jsonPath("$.data.name").value("통합 수정방")) + .andExpect(jsonPath("$.data.thumbnailUrl").value("https://cdn.example.com/updated-room.png")); + } + @Test void thumbnailUploadRejectsInvalidImageFile() throws Exception { User host = saveUser("방장", "thumbnail-invalid-host"); diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemCommentApiTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemCommentApiTest.java new file mode 100644 index 00000000..3990a3e5 --- /dev/null +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemCommentApiTest.java @@ -0,0 +1,268 @@ +package com.aisip.OnO.backend.studyroom.integration; + +import com.aisip.OnO.backend.problem.dto.ProblemRegisterDto; +import com.aisip.OnO.backend.problem.entity.Problem; +import com.aisip.OnO.backend.problem.repository.ProblemRepository; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.repository.UserRepository; +import com.aisip.OnO.backend.util.RandomUserGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +@ActiveProfiles("test") +class StudyRoomSharedProblemCommentApiTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ProblemRepository problemRepository; + + private Long authenticatedUserId; + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void memberCanCreateUpdateAndListSharedProblemComments() throws Exception { + User host = saveUser("방장", "comment-flow-host"); + User member = saveUser("멤버", "comment-flow-member"); + authenticate(host.getId()); + Long roomId = createRoom("댓글 테스트방"); + String inviteCode = issueInviteCode(roomId); + Long sharedProblemId = shareProblem(roomId, createProblem(host.getId()).getId(), "공유 문제"); + + authenticate(member.getId()); + join(inviteCode); + Long commentId = createComment(roomId, sharedProblemId, "처음 풀이 의견"); + + mockMvc.perform(patch("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments/{commentId}", + roomId, sharedProblemId, commentId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new SharedProblemCommentRequest("수정된 풀이 의견")))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.commentId").value(commentId)) + .andExpect(jsonPath("$.data.content").value("수정된 풀이 의견")) + .andExpect(jsonPath("$.data.authorId").value(member.getId())) + .andExpect(jsonPath("$.data.authorName").value("멤버")) + .andExpect(jsonPath("$.data.isMine").value(true)); + + MvcResult result = mockMvc.perform(get("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments", + roomId, sharedProblemId) + .param("size", "10") + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.content[0].commentId").value(commentId)) + .andExpect(jsonPath("$.data.content[0].content").value("수정된 풀이 의견")) + .andExpect(jsonPath("$.data.content[0].isMine").value(true)) + .andReturn(); + + List comments = JsonPath.read(result.getResponse().getContentAsString(), "$.data.content"); + assertThat(comments).hasSize(1); + } + + @Test + void onlyAuthorCanUpdateAndHostCanDeleteOtherMemberComment() throws Exception { + User host = saveUser("방장", "comment-auth-host"); + User member = saveUser("멤버", "comment-auth-member"); + User anotherMember = saveUser("다른멤버", "comment-auth-another"); + authenticate(host.getId()); + Long roomId = createRoom("댓글 권한방"); + String inviteCode = issueInviteCode(roomId); + Long sharedProblemId = shareProblem(roomId, createProblem(host.getId()).getId(), "공유 문제"); + + authenticate(member.getId()); + join(inviteCode); + Long commentId = createComment(roomId, sharedProblemId, "멤버 댓글"); + + authenticate(anotherMember.getId()); + join(inviteCode); + mockMvc.perform(patch("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments/{commentId}", + roomId, sharedProblemId, commentId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new SharedProblemCommentRequest("권한 없는 수정")))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(10002)); + + authenticate(host.getId()); + mockMvc.perform(delete("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments/{commentId}", + roomId, sharedProblemId, commentId) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data").value("공유 문제 댓글 삭제가 완료되었습니다.")); + + mockMvc.perform(get("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments", roomId, sharedProblemId) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.content").isEmpty()); + } + + @Test + void commentPaginationUsesCursor() throws Exception { + User host = saveUser("방장", "comment-page-host"); + authenticate(host.getId()); + Long roomId = createRoom("댓글 페이지방"); + Long sharedProblemId = shareProblem(roomId, createProblem(host.getId()).getId(), "공유 문제"); + for (int i = 0; i < 5; i++) { + createComment(roomId, sharedProblemId, "댓글 " + i); + } + + MvcResult firstResult = mockMvc.perform(get("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments", + roomId, sharedProblemId) + .param("size", "3") + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.hasNext").value(true)) + .andReturn(); + Number nextCursor = JsonPath.read(firstResult.getResponse().getContentAsString(), "$.data.nextCursor"); + + MvcResult secondResult = mockMvc.perform(get("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments", + roomId, sharedProblemId) + .param("size", "3") + .param("cursor", nextCursor.toString()) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.hasNext").value(false)) + .andReturn(); + + List comments = JsonPath.read(secondResult.getResponse().getContentAsString(), "$.data.content"); + assertThat(comments).hasSize(2); + } + + @Test + void blankOrTooLongCommentIsRejected() throws Exception { + User host = saveUser("방장", "comment-invalid-host"); + authenticate(host.getId()); + Long roomId = createRoom("댓글 검증방"); + Long sharedProblemId = shareProblem(roomId, createProblem(host.getId()).getId(), "공유 문제"); + + mockMvc.perform(post("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments", roomId, sharedProblemId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new SharedProblemCommentRequest(" ")))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value(10016)); + + mockMvc.perform(post("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments", roomId, sharedProblemId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new SharedProblemCommentRequest("a".repeat(1001))))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value(10016)); + } + + private User saveUser(String name, String emailPrefix) { + return userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", name, emailPrefix)); + } + + private void authenticate(Long userId) { + authenticatedUserId = userId; + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken( + userId, + null, + List.of(new SimpleGrantedAuthority("ROLE_MEMBER")) + ) + ); + } + + private RequestPostProcessor auth() { + return authentication(new UsernamePasswordAuthenticationToken( + authenticatedUserId, null, List.of(new SimpleGrantedAuthority("ROLE_MEMBER")) + )); + } + + private Long createRoom(String name) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomCreateRequest(name)))) + .andExpect(status().isCreated()) + .andReturn(); + return readLong(result, "$.data.roomId"); + } + + private String issueInviteCode(Long roomId) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room/{roomId}/invite", roomId) + .with(auth())) + .andExpect(status().isOk()) + .andReturn(); + return JsonPath.read(result.getResponse().getContentAsString(), "$.data.code"); + } + + private void join(String inviteCode) throws Exception { + mockMvc.perform(post("/api/study-room/join") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomJoinRequest(inviteCode)))) + .andExpect(status().isOk()); + } + + private Problem createProblem(Long userId) { + return problemRepository.save(Problem.from( + new ProblemRegisterDto(null, "memo", "reference", null, LocalDateTime.now()), + userId + )); + } + + private Long shareProblem(Long roomId, Long problemId, String comment) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room/{roomId}/shared-problems", roomId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new SharedProblemCreateRequest(problemId, comment)))) + .andExpect(status().isCreated()) + .andReturn(); + return readLong(result, "$.data.sharedProblemId"); + } + + private Long createComment(Long roomId, Long sharedProblemId, String content) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments", + roomId, sharedProblemId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new SharedProblemCommentRequest(content)))) + .andExpect(status().isCreated()) + .andReturn(); + return readLong(result, "$.data.commentId"); + } + + private Long readLong(MvcResult result, String path) throws Exception { + Number value = JsonPath.read(result.getResponse().getContentAsString(), path); + return value.longValue(); + } +} From 4deaeb05900efd84b633675e4c62a5e9e931767c Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sun, 28 Jun 2026 23:13:47 +0900 Subject: [PATCH 33/67] =?UTF-8?q?[Test]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20=EC=B1=8C=EB=A6=B0=EC=A7=80=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95=20-=20=EC=82=AD=EC=A0=9C=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EC=A3=BC=EA=B8=B0=EB=B3=84=20=EC=A7=84=ED=96=89=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StudyRoomChallengeApiIntegrationTest.java | 76 ++++++ .../StudyRoomChallengeServiceTest.java | 227 ++++++++++++++++++ 2 files changed, 303 insertions(+) diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomChallengeApiIntegrationTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomChallengeApiIntegrationTest.java index ff8a0e47..a426303b 100644 --- a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomChallengeApiIntegrationTest.java +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomChallengeApiIntegrationTest.java @@ -36,6 +36,8 @@ import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -155,6 +157,80 @@ void createChallengeRejectsInvalidPeriod() throws Exception { .andExpect(jsonPath("$.errorCode").value(10016)); } + @Test + void hostCanDeleteChallengeAndMemberCannot() throws Exception { + ChallengeCreateRequest request = new ChallengeCreateRequest( + "삭제 테스트", "individual", "practice_count", null, + 3, null, LocalDateTime.now().plusDays(7) + ); + // 방장으로 챌린지 생성: 세션 간 인증 공유 문제를 피하기 위해 auth() RequestPostProcessor 사용 + MvcResult createResult = mockMvc.perform(post("/api/study-room/{roomId}/challenges", roomId) + .with(authentication(new UsernamePasswordAuthenticationToken( + host.getId(), null, List.of(new SimpleGrantedAuthority("ROLE_MEMBER"))))) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andReturn(); + Number challengeIdValue = JsonPath.read(createResult.getResponse().getContentAsString(), "$.data.challengeId"); + Long challengeId = challengeIdValue.longValue(); + + // 멤버는 삭제 불가 - HOST_ONLY (10003) + mockMvc.perform(delete("/api/study-room/{roomId}/challenges/{challengeId}", roomId, challengeId) + .with(authentication(new UsernamePasswordAuthenticationToken( + member.getId(), null, List.of(new SimpleGrantedAuthority("ROLE_MEMBER")))))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(10003)); + + // 방장은 삭제 가능 + mockMvc.perform(delete("/api/study-room/{roomId}/challenges/{challengeId}", roomId, challengeId) + .with(authentication(new UsernamePasswordAuthenticationToken( + host.getId(), null, List.of(new SimpleGrantedAuthority("ROLE_MEMBER")))))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data").value("챌린지 삭제가 완료되었습니다.")); + } + + @Test + void createChallengeRejectsTitleTooLong() throws Exception { + ChallengeCreateRequest request = new ChallengeCreateRequest( + "a".repeat(41), "individual", "practice_count", null, + 3, null, LocalDateTime.now().plusDays(7) + ); + authenticate(member.getId()); + mockMvc.perform(post("/api/study-room/{roomId}/challenges", roomId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value(10016)); + } + + @Test + void createChallengeRejectsPastEndAt() throws Exception { + ChallengeCreateRequest request = new ChallengeCreateRequest( + "과거 종료일", "individual", "practice_count", null, + 3, null, LocalDateTime.now().minusDays(1) + ); + authenticate(member.getId()); + mockMvc.perform(post("/api/study-room/{roomId}/challenges", roomId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value(10016)); + } + + @Test + void createChallengeRejectsStartAtAfterEndAt() throws Exception { + ChallengeCreateRequest request = new ChallengeCreateRequest( + "날짜 역전", "individual", "practice_count", null, + 3, LocalDateTime.now().plusDays(5), LocalDateTime.now().plusDays(3) + ); + authenticate(member.getId()); + mockMvc.perform(post("/api/study-room/{roomId}/challenges", roomId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value(10016)); + } + private void authenticate(Long userId) { SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken( diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeServiceTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeServiceTest.java index 0488338a..4c6bc129 100644 --- a/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeServiceTest.java +++ b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeServiceTest.java @@ -14,6 +14,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.List; @@ -117,6 +118,232 @@ void nonPeriodGroupChallengeCompletesWhenGroupCurrentReachesTarget() { assertThat(response.groupCurrent()).isEqualTo(5); } + @Test + void dailyPeriodChallengeAggregatesCurrentDayWindow() { + // DAILY 주기: startAt = 3일 전 자정, endAt = 4일 후 자정 + LocalDateTime startAt = LocalDateTime.now().truncatedTo(ChronoUnit.DAYS).minusDays(3); + LocalDateTime endAt = startAt.plusDays(7); + User user = user(1L, "멤버"); + StudyRoomMember member = StudyRoomMember.create(user, StudyRoomMemberRole.HOST); + StudyRoomChallenge challenge = challenge( + StudyRoomChallengeType.INDIVIDUAL, + StudyRoomChallengeMetric.PRACTICE_COUNT, + StudyRoomChallengePeriod.DAILY, + 2, + startAt, + endAt + ); + + given(memberRepository.findAllWithUserByRoomId(10L)).willReturn(List.of(member)); + given(challengeRepository.findActiveByRoomId(eq(10L), eq(StudyRoomChallengeStatus.IN_PROGRESS), any(LocalDateTime.class))) + .willReturn(List.of(challenge)); + given(statsService.currentStreaks(anyList(), any(), any())).willReturn(Map.of(user.getId(), 0)); + // practice_count=1, target=2 → cleared=false + given(statsService.getStats(anyList(), any(LocalDateTime.class), any(LocalDateTime.class), anyMap())) + .willReturn(Map.of(user.getId(), new StudyRoomStats(0, 1, 0))); + + List responses = challengeService.getChallenges(10L, user.getId()); + + assertThat(responses).hasSize(1); + ChallengeResponse response = responses.get(0); + assertThat(response.status()).isEqualTo("in_progress"); + assertThat(response.memberProgress()).singleElement().satisfies(p -> { + assertThat(p.current()).isEqualTo(1); + assertThat(p.cleared()).isFalse(); + }); + + // 오늘(day 3) 구간: [startAt+3days, startAt+4days] + ArgumentCaptor rangeStart = ArgumentCaptor.forClass(LocalDateTime.class); + ArgumentCaptor rangeEnd = ArgumentCaptor.forClass(LocalDateTime.class); + verify(statsService).getStats(anyList(), rangeStart.capture(), rangeEnd.capture(), anyMap()); + assertThat(rangeStart.getValue()).isEqualTo(startAt.plusDays(3)); + assertThat(rangeEnd.getValue()).isEqualTo(startAt.plusDays(4)); + } + + @Test + void individualChallengeStaysInProgressWhenNotAllMembersCleared() { + LocalDateTime startAt = LocalDateTime.now().minusDays(1).truncatedTo(ChronoUnit.SECONDS); + LocalDateTime endAt = LocalDateTime.now().plusDays(1).truncatedTo(ChronoUnit.SECONDS); + User member1 = user(1L, "첫번째"); + User member2 = user(2L, "두번째"); + StudyRoomChallenge challenge = challenge( + StudyRoomChallengeType.INDIVIDUAL, + StudyRoomChallengeMetric.PRACTICE_COUNT, + null, + 3, + startAt, + endAt + ); + + given(memberRepository.findAllWithUserByRoomId(10L)) + .willReturn(List.of( + StudyRoomMember.create(member1, StudyRoomMemberRole.HOST), + StudyRoomMember.create(member2, StudyRoomMemberRole.MEMBER) + )); + given(challengeRepository.findActiveByRoomId(eq(10L), eq(StudyRoomChallengeStatus.IN_PROGRESS), any(LocalDateTime.class))) + .willReturn(List.of(challenge)); + given(statsService.currentStreaks(anyList(), any(), any())) + .willReturn(Map.of(member1.getId(), 0, member2.getId(), 0)); + // member1: practice_count=2 (미달), member2: practice_count=5 (달성) + given(statsService.getStats(anyList(), any(LocalDateTime.class), any(LocalDateTime.class), anyMap())) + .willReturn(Map.of( + member1.getId(), new StudyRoomStats(0, 2, 0), + member2.getId(), new StudyRoomStats(0, 5, 0) + )); + + ChallengeResponse response = challengeService.getChallenges(10L, member1.getId()).get(0); + + // 일부 멤버만 달성했으므로 in_progress 유지 + assertThat(response.status()).isEqualTo("in_progress"); + assertThat(response.memberProgress()).anySatisfy(p -> { + assertThat(p.userId()).isEqualTo(member1.getId()); + assertThat(p.cleared()).isFalse(); + }); + assertThat(response.memberProgress()).anySatisfy(p -> { + assertThat(p.userId()).isEqualTo(member2.getId()); + assertThat(p.cleared()).isTrue(); + }); + } + + @Test + void expiredEndAtChallengeReturnsExpiredStatus() { + LocalDateTime startAt = LocalDateTime.now().minusDays(10).truncatedTo(ChronoUnit.SECONDS); + LocalDateTime endAt = LocalDateTime.now().minusDays(2).truncatedTo(ChronoUnit.SECONDS); + User user = user(1L, "멤버"); + StudyRoomChallenge challenge = challenge( + StudyRoomChallengeType.INDIVIDUAL, + StudyRoomChallengeMetric.PRACTICE_COUNT, + null, + 10, + startAt, + endAt + ); + + given(memberRepository.findAllWithUserByRoomId(10L)) + .willReturn(List.of(StudyRoomMember.create(user, StudyRoomMemberRole.HOST))); + given(challengeRepository.findActiveByRoomId(eq(10L), eq(StudyRoomChallengeStatus.IN_PROGRESS), any(LocalDateTime.class))) + .willReturn(List.of(challenge)); + given(statsService.currentStreaks(anyList(), any(), any())).willReturn(Map.of(user.getId(), 0)); + // practice_count=1, target=10 → 미달성이고 endAt도 과거 + given(statsService.getStats(anyList(), any(LocalDateTime.class), any(LocalDateTime.class), anyMap())) + .willReturn(Map.of(user.getId(), new StudyRoomStats(0, 1, 0))); + + ChallengeResponse response = challengeService.getChallenges(10L, user.getId()).get(0); + + assertThat(response.status()).isEqualTo("expired"); + } + + @Test + void attendanceMetricUsesAttendanceDayCountFromStatsService() { + LocalDateTime startAt = LocalDateTime.now().minusDays(1).truncatedTo(ChronoUnit.SECONDS); + LocalDateTime endAt = LocalDateTime.now().plusDays(1).truncatedTo(ChronoUnit.SECONDS); + User user = user(1L, "멤버"); + StudyRoomChallenge challenge = challenge( + StudyRoomChallengeType.INDIVIDUAL, + StudyRoomChallengeMetric.ATTENDANCE, + null, + 5, + startAt, + endAt + ); + + given(memberRepository.findAllWithUserByRoomId(10L)) + .willReturn(List.of(StudyRoomMember.create(user, StudyRoomMemberRole.HOST))); + given(challengeRepository.findActiveByRoomId(eq(10L), eq(StudyRoomChallengeStatus.IN_PROGRESS), any(LocalDateTime.class))) + .willReturn(List.of(challenge)); + given(statsService.currentStreaks(anyList(), any(), any())).willReturn(Map.of(user.getId(), 0)); + given(statsService.getStats(anyList(), any(LocalDateTime.class), any(LocalDateTime.class), anyMap())) + .willReturn(Map.of(user.getId(), new StudyRoomStats(0, 0, 0))); + // attendanceDayCounts → 6 (target=5이므로 cleared=true) + given(statsService.attendanceDayCounts(anyList(), any(LocalDateTime.class), any(LocalDateTime.class))) + .willReturn(Map.of(user.getId(), 6)); + + List responses = challengeService.getChallenges(10L, user.getId()); + + assertThat(responses).hasSize(1); + assertThat(responses.get(0).memberProgress()).singleElement().satisfies(p -> { + assertThat(p.current()).isEqualTo(6); + assertThat(p.cleared()).isTrue(); + }); + verify(statsService).attendanceDayCounts(anyList(), any(LocalDateTime.class), any(LocalDateTime.class)); + } + + @Test + void monthlyPeriodChallengeAggregatesCurrentMonthWindow() { + // 2달 전 첫째날 자정부터 시작하는 MONTHLY 주기 챌린지 + LocalDateTime startAt = LocalDate.now().minusMonths(2).withDayOfMonth(1).atStartOfDay(); + LocalDateTime endAt = startAt.plusMonths(6); + User user = user(1L, "멤버"); + StudyRoomMember member = StudyRoomMember.create(user, StudyRoomMemberRole.HOST); + StudyRoomChallenge challenge = challenge( + StudyRoomChallengeType.INDIVIDUAL, + StudyRoomChallengeMetric.PRACTICE_COUNT, + StudyRoomChallengePeriod.MONTHLY, + 3, + startAt, + endAt + ); + + given(memberRepository.findAllWithUserByRoomId(10L)).willReturn(List.of(member)); + given(challengeRepository.findActiveByRoomId(eq(10L), eq(StudyRoomChallengeStatus.IN_PROGRESS), any(LocalDateTime.class))) + .willReturn(List.of(challenge)); + given(statsService.currentStreaks(anyList(), any(), any())).willReturn(Map.of(user.getId(), 0)); + // practice_count=1, target=3 → 미달성 → status = in_progress + given(statsService.getStats(anyList(), any(LocalDateTime.class), any(LocalDateTime.class), anyMap())) + .willReturn(Map.of(user.getId(), new StudyRoomStats(0, 1, 0))); + + List responses = challengeService.getChallenges(10L, user.getId()); + + assertThat(responses).hasSize(1); + assertThat(responses.get(0).status()).isEqualTo("in_progress"); + assertThat(responses.get(0).memberProgress()).singleElement().satisfies(p -> { + assertThat(p.current()).isEqualTo(1); + assertThat(p.cleared()).isFalse(); + }); + + // 현재 구간 = [startAt.plusMonths(2), startAt.plusMonths(3)] 임을 검증 + ArgumentCaptor rangeStart = ArgumentCaptor.forClass(LocalDateTime.class); + ArgumentCaptor rangeEnd = ArgumentCaptor.forClass(LocalDateTime.class); + verify(statsService).getStats(anyList(), rangeStart.capture(), rangeEnd.capture(), anyMap()); + assertThat(rangeStart.getValue()).isEqualTo(startAt.plusMonths(2)); + assertThat(rangeEnd.getValue()).isEqualTo(startAt.plusMonths(3)); + } + + @Test + void streakMetricUsesAttendanceDayCount() { + LocalDateTime startAt = LocalDateTime.now().minusDays(14).truncatedTo(ChronoUnit.SECONDS); + LocalDateTime endAt = startAt.plusDays(28); + User user = user(7L, "멤버"); + StudyRoomMember member = StudyRoomMember.create(user, StudyRoomMemberRole.HOST); + StudyRoomChallenge challenge = challenge( + StudyRoomChallengeType.INDIVIDUAL, + StudyRoomChallengeMetric.STREAK, + null, + 5, + startAt, + endAt + ); + + given(memberRepository.findAllWithUserByRoomId(10L)).willReturn(List.of(member)); + given(challengeRepository.findActiveByRoomId(eq(10L), eq(StudyRoomChallengeStatus.IN_PROGRESS), any(LocalDateTime.class))) + .willReturn(List.of(challenge)); + given(statsService.currentStreaks(anyList(), any(), any())).willReturn(Map.of(user.getId(), 7)); + given(statsService.getStats(anyList(), any(LocalDateTime.class), any(LocalDateTime.class), anyMap())) + .willReturn(Map.of(user.getId(), new StudyRoomStats(0, 0, 7))); + // attendanceDayCounts = 7, target = 5 → cleared=true + given(statsService.attendanceDayCounts(anyList(), any(LocalDateTime.class), any(LocalDateTime.class))) + .willReturn(Map.of(user.getId(), 7)); + + List responses = challengeService.getChallenges(10L, user.getId()); + + assertThat(responses).hasSize(1); + assertThat(responses.get(0).memberProgress()).singleElement().satisfies(p -> { + assertThat(p.current()).isEqualTo(7); + assertThat(p.cleared()).isTrue(); + }); + verify(statsService).attendanceDayCounts(anyList(), any(LocalDateTime.class), any(LocalDateTime.class)); + } + private StudyRoomChallenge challenge(StudyRoomChallengeType type, StudyRoomChallengeMetric metric, StudyRoomChallengePeriod period, int targetValue, LocalDateTime startAt, LocalDateTime endAt) { From 38100fad4164cca6cedb4fec005c22e49ff458bb Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sun, 28 Jun 2026 23:14:04 +0900 Subject: [PATCH 34/67] =?UTF-8?q?[Test]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20API=20=EA=B2=80=EC=A6=9D=20=EB=B3=B4=EA=B0=95=20-?= =?UTF-8?q?=20=ED=94=BC=EB=93=9C=20=EC=B4=88=EB=8C=80=20=EB=A9=A4=EB=B2=84?= =?UTF-8?q?=20=EC=84=B8=EC=85=98=20=EA=B3=B5=EC=9C=A0=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=8F=AC=ED=8A=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration/StudyRoomFeedApiTest.java | 221 +++++++++++++++ .../integration/StudyRoomInviteApiTest.java | 163 +++++++++++ .../StudyRoomMemberManagementApiTest.java | 265 ++++++++++++++++++ .../StudyRoomSharedProblemApiTest.java | 243 ++++++++++++++++ .../StudyRoomWeeklyReportApiTest.java | 218 ++++++++++++++ .../integration/StudySessionApiTest.java | 138 +++++++++ .../service/StudyRoomFeedServiceUnitTest.java | 128 +++++++++ 7 files changed, 1376 insertions(+) create mode 100644 src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomFeedApiTest.java create mode 100644 src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomInviteApiTest.java create mode 100644 src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomMemberManagementApiTest.java create mode 100644 src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemApiTest.java create mode 100644 src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomWeeklyReportApiTest.java create mode 100644 src/test/java/com/aisip/OnO/backend/studyroom/integration/StudySessionApiTest.java create mode 100644 src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedServiceUnitTest.java diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomFeedApiTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomFeedApiTest.java new file mode 100644 index 00000000..3c44ac02 --- /dev/null +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomFeedApiTest.java @@ -0,0 +1,221 @@ +package com.aisip.OnO.backend.studyroom.integration; + +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.studyroom.entity.StudyRoom; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomFeed; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomFeedEventType; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomFeedRepository; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomRepository; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.repository.UserRepository; +import com.aisip.OnO.backend.util.RandomUserGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +@ActiveProfiles("test") +class StudyRoomFeedApiTest { + + private static final String EMOJI = "fired_up_sparkle_eyes"; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private UserRepository userRepository; + + @Autowired + private StudyRoomRepository roomRepository; + + @Autowired + private StudyRoomFeedRepository feedRepository; + + private Long authenticatedUserId; + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void nonMemberCannotGetFeed() throws Exception { + User host = saveUser("방장", "feed-nonmember-host"); + User outsider = saveUser("외부인", "feed-nonmember-outsider"); + authenticate(host.getId()); + Long roomId = createRoom("피드 접근 테스트방"); + + // 멤버가 아닌 외부인이 피드 조회 → 403 (10002) + authenticate(outsider.getId()); + mockMvc.perform(get("/api/study-room/{roomId}/feed", roomId).with(auth())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(10002)); + } + + @Test + void feedPaginationWithCursor() throws Exception { + User host = saveUser("방장", "feed-pagination-host"); + authenticate(host.getId()); + Long roomId = createRoom("피드 페이지네이션 테스트방"); + + // 피드 5개 직접 저장 (DB에 직접 저장 → 연속 ID 보장) + StudyRoom room = roomRepository.findById(roomId).orElseThrow(); + for (int i = 0; i < 5; i++) { + feedRepository.save(StudyRoomFeed.create(room, host, StudyRoomFeedEventType.SESSION_STARTED, "{}")); + } + + // 첫 번째 페이지: size=3 → 3개, hasNext=true + MvcResult firstResult = mockMvc.perform(get("/api/study-room/{roomId}/feed", roomId) + .param("size", "3") + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.hasNext").value(true)) + .andReturn(); + + String firstBody = firstResult.getResponse().getContentAsString(); + List firstContent = JsonPath.read(firstBody, "$.data.content"); + assertThat(firstContent).hasSize(3); + Number nextCursor = JsonPath.read(firstBody, "$.data.nextCursor"); + + // 두 번째 페이지: cursor=nextCursor → 2개, hasNext=false + MvcResult secondResult = mockMvc.perform(get("/api/study-room/{roomId}/feed", roomId) + .param("size", "3") + .param("cursor", nextCursor.toString()) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.hasNext").value(false)) + .andReturn(); + + String secondBody = secondResult.getResponse().getContentAsString(); + List secondContent = JsonPath.read(secondBody, "$.data.content"); + assertThat(secondContent).hasSize(2); + } + + @Test + void multipleUsersCanReactToSameFeed() throws Exception { + User host = saveUser("방장", "feed-react-host"); + User member = saveUser("멤버", "feed-react-member"); + authenticate(host.getId()); + Long roomId = createRoom("피드 반응 테스트방"); + String code = issueInviteCode(roomId); + authenticate(member.getId()); + join(code); + + // 피드 1개 직접 저장 + StudyRoom room = roomRepository.findById(roomId).orElseThrow(); + StudyRoomFeed feed = feedRepository.save( + StudyRoomFeed.create(room, host, StudyRoomFeedEventType.SESSION_STARTED, "{}")); + + // host가 반응 추가 → count=1 + authenticate(host.getId()); + mockMvc.perform(post("/api/study-room/{roomId}/feed/{feedId}/reactions", roomId, feed.getId()) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ReactionToggleRequest(EMOJI)))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.reactions[0].count").value(1)); + + // member가 동일 이모지로 반응 추가 → count=2, reactedByMe=true (member 기준) + authenticate(member.getId()); + mockMvc.perform(post("/api/study-room/{roomId}/feed/{feedId}/reactions", roomId, feed.getId()) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ReactionToggleRequest(EMOJI)))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.reactions[0].count").value(2)) + .andExpect(jsonPath("$.data.reactions[0].reactedByMe").value(true)); + } + + @Test + void nonMemberCannotToggleFeedReaction() throws Exception { + User host = saveUser("방장", "feed-react-perm-host"); + User outsider = saveUser("외부인", "feed-react-perm-outsider"); + authenticate(host.getId()); + Long roomId = createRoom("반응 권한 테스트방"); + + // 피드 1개 직접 저장 + StudyRoom room = roomRepository.findById(roomId).orElseThrow(); + StudyRoomFeed feed = feedRepository.save( + StudyRoomFeed.create(room, host, StudyRoomFeedEventType.SESSION_STARTED, "{}")); + + // 외부인이 반응 시도 → 403 (10002) + authenticate(outsider.getId()); + mockMvc.perform(post("/api/study-room/{roomId}/feed/{feedId}/reactions", roomId, feed.getId()) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ReactionToggleRequest(EMOJI)))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(10002)); + } + + // ========== 헬퍼 ========== + + private User saveUser(String name, String emailPrefix) { + return userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", name, emailPrefix)); + } + + private void authenticate(Long userId) { + authenticatedUserId = userId; + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken( + userId, null, List.of(new SimpleGrantedAuthority("ROLE_MEMBER")) + ) + ); + } + + private RequestPostProcessor auth() { + return authentication(new UsernamePasswordAuthenticationToken( + authenticatedUserId, null, List.of(new SimpleGrantedAuthority("ROLE_MEMBER")) + )); + } + + private Long createRoom(String name) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomCreateRequest(name)))) + .andExpect(status().isCreated()) + .andReturn(); + Number value = JsonPath.read(result.getResponse().getContentAsString(), "$.data.roomId"); + return value.longValue(); + } + + private String issueInviteCode(Long roomId) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room/{roomId}/invite", roomId).with(auth())) + .andExpect(status().isOk()) + .andReturn(); + return JsonPath.read(result.getResponse().getContentAsString(), "$.data.code"); + } + + private void join(String code) throws Exception { + mockMvc.perform(post("/api/study-room/join") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomJoinRequest(code)))) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomInviteApiTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomInviteApiTest.java new file mode 100644 index 00000000..cb0420e1 --- /dev/null +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomInviteApiTest.java @@ -0,0 +1,163 @@ +package com.aisip.OnO.backend.studyroom.integration; + +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.repository.UserRepository; +import com.aisip.OnO.backend.util.RandomUserGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +@ActiveProfiles("test") +class StudyRoomInviteApiTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private UserRepository userRepository; + + private Long authenticatedUserId; + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void inviteCodeIsReusedWhenNotExpired() throws Exception { + User host = saveUser("방장", "invite-reuse-host"); + authenticate(host.getId()); + Long roomId = createRoom("코드 재사용 테스트방"); + + String code1 = issueInviteCode(roomId); + String code2 = issueInviteCode(roomId); + + // 아직 만료되지 않았으므로 동일 코드 반환 + assertThat(code2).isEqualTo(code1); + } + + @Test + void invalidFormatInviteCodeIsRejected() throws Exception { + User user = saveUser("사용자", "invite-invalid-user"); + authenticate(user.getId()); + + // 6자리 숫자가 아닌 코드 "abc" → 10006 + mockMvc.perform(post("/api/study-room/join") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomJoinRequest("abc")))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value(10006)); + + // 7자리 숫자 코드 "1234567" → 10006 + mockMvc.perform(post("/api/study-room/join") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomJoinRequest("1234567")))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value(10006)); + + // null 코드 → 10006 + mockMvc.perform(post("/api/study-room/join") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomJoinRequest(null)))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value(10006)); + } + + @Test + void alreadyMemberCannotJoinRoom() throws Exception { + User host = saveUser("방장", "invite-already-member-host"); + authenticate(host.getId()); + Long roomId = createRoom("중복 참여 테스트방"); + String code = issueInviteCode(roomId); + + // host가 자신의 방에 초대코드로 재가입 시도 → ALREADY_MEMBER (10008) + mockMvc.perform(post("/api/study-room/join") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomJoinRequest(code)))) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.errorCode").value(10008)); + } + + @Test + void nonExistentValidFormatCodeIsRejected() throws Exception { + User user = saveUser("사용자", "invite-nonexist-user"); + authenticate(user.getId()); + + // 형식은 맞지만 DB에 없는 코드 → INVITE_CODE_INVALID (10006) + mockMvc.perform(post("/api/study-room/join") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomJoinRequest("000000")))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value(10006)); + } + + // ========== 헬퍼 ========== + + private User saveUser(String name, String emailPrefix) { + return userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", name, emailPrefix)); + } + + private void authenticate(Long userId) { + authenticatedUserId = userId; + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken( + userId, null, List.of(new SimpleGrantedAuthority("ROLE_MEMBER")) + ) + ); + } + + private RequestPostProcessor auth() { + return authentication(new UsernamePasswordAuthenticationToken( + authenticatedUserId, null, List.of(new SimpleGrantedAuthority("ROLE_MEMBER")) + )); + } + + private Long createRoom(String name) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomCreateRequest(name)))) + .andExpect(status().isCreated()) + .andReturn(); + Number value = JsonPath.read(result.getResponse().getContentAsString(), "$.data.roomId"); + return value.longValue(); + } + + private String issueInviteCode(Long roomId) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room/{roomId}/invite", roomId).with(auth())) + .andExpect(status().isOk()) + .andReturn(); + return JsonPath.read(result.getResponse().getContentAsString(), "$.data.code"); + } +} diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomMemberManagementApiTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomMemberManagementApiTest.java new file mode 100644 index 00000000..5188241f --- /dev/null +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomMemberManagementApiTest.java @@ -0,0 +1,265 @@ +package com.aisip.OnO.backend.studyroom.integration; + +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.repository.UserRepository; +import com.aisip.OnO.backend.util.RandomUserGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + +import java.util.List; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +@ActiveProfiles("test") +class StudyRoomMemberManagementApiTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private UserRepository userRepository; + + private Long authenticatedUserId; + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + // ========== 강퇴 권한 ========== + + @Test + void hostCanKickMemberAndMemberCannotKick() throws Exception { + User host = saveUser("방장", "mgmt-kick-host"); + User member = saveUser("멤버", "mgmt-kick-member"); + authenticate(host.getId()); + Long roomId = createRoom("강퇴 테스트방"); + String code = issueInviteCode(roomId); + authenticate(member.getId()); + join(code); + + // member가 host 강퇴 시도 → 방장만 가능 (10003) + mockMvc.perform(delete("/api/study-room/{roomId}/members/{memberId}", roomId, host.getId()) + .with(auth())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(10003)); + + // host가 member 강퇴 → 성공 + authenticate(host.getId()); + mockMvc.perform(delete("/api/study-room/{roomId}/members/{memberId}", roomId, member.getId()) + .with(auth())) + .andExpect(status().isOk()); + + // 방 조회 → memberCount=1 + mockMvc.perform(get("/api/study-room/{roomId}", roomId).with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.memberCount").value(1)); + } + + @Test + void hostCannotKickSelf() throws Exception { + User host = saveUser("방장", "mgmt-selfkick-host"); + authenticate(host.getId()); + Long roomId = createRoom("자기 강퇴 테스트방"); + + // host가 자신을 강퇴 시도 → host 역할이라 막힘 (10003) + mockMvc.perform(delete("/api/study-room/{roomId}/members/{memberId}", roomId, host.getId()) + .with(auth())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(10003)); + } + + // ========== 방 삭제 권한 ========== + + @Test + void hostCanDeleteRoomAndMemberCannot() throws Exception { + // 시나리오 A: 방에 속한 member가 삭제 시도 → 방장만 가능 (10003) + User host = saveUser("방장", "mgmt-delete-host"); + User member = saveUser("멤버", "mgmt-delete-member"); + authenticate(host.getId()); + Long roomIdWithMember = createRoom("삭제 권한 테스트방"); + String code = issueInviteCode(roomIdWithMember); + authenticate(member.getId()); + join(code); + + mockMvc.perform(delete("/api/study-room/{roomId}", roomIdWithMember).with(auth())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(10003)); + + // 시나리오 B: host 단독 방(초대코드 없음) 삭제 → 성공 + // 초대코드 FK 제약이 없는 방에서 검증 + User host2 = saveUser("방장2", "mgmt-delete-host2"); + authenticate(host2.getId()); + Long roomIdAlone = createRoom("host 단독 삭제 테스트방"); + + mockMvc.perform(delete("/api/study-room/{roomId}", roomIdAlone).with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data").value("스터디룸 삭제가 완료되었습니다.")); + } + + // ========== 탈퇴 ========== + + @Test + void memberLeaveRemovesMemberFromRoom() throws Exception { + User host = saveUser("방장", "mgmt-leave-host"); + User member = saveUser("멤버", "mgmt-leave-member"); + authenticate(host.getId()); + Long roomId = createRoom("탈퇴 테스트방"); + String code = issueInviteCode(roomId); + authenticate(member.getId()); + join(code); + + // member 탈퇴 + mockMvc.perform(delete("/api/study-room/{roomId}/leave", roomId).with(auth())) + .andExpect(status().isOk()); + + // 탈퇴 후 접근 → 멤버가 아님 (10002) + mockMvc.perform(get("/api/study-room/{roomId}", roomId).with(auth())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(10002)); + } + + @Test + void hostLeaveTransfersHostToNextMember() throws Exception { + User host = saveUser("방장", "mgmt-transfer-host"); + User member = saveUser("멤버", "mgmt-transfer-member"); + authenticate(host.getId()); + Long roomId = createRoom("위임 테스트방"); + String code = issueInviteCode(roomId); + authenticate(member.getId()); + join(code); + + // host 탈퇴 → member에게 방장 위임 + authenticate(host.getId()); + mockMvc.perform(delete("/api/study-room/{roomId}/leave", roomId).with(auth())) + .andExpect(status().isOk()); + + // member 기준으로 방 조회 → member가 방장, memberCount=1 + authenticate(member.getId()); + mockMvc.perform(get("/api/study-room/{roomId}", roomId).with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.hostUserId").value(member.getId())) + .andExpect(jsonPath("$.data.memberCount").value(1)); + } + + @Test + void hostLeaveAloneDeletesRoom() throws Exception { + User host = saveUser("방장", "mgmt-alone-host"); + authenticate(host.getId()); + Long roomId = createRoom("혼자 탈퇴 테스트방"); + + // host 혼자 탈퇴 → 방 삭제 + mockMvc.perform(delete("/api/study-room/{roomId}/leave", roomId).with(auth())) + .andExpect(status().isOk()); + + // 방 조회 → 404 (10001) + mockMvc.perform(get("/api/study-room/{roomId}", roomId).with(auth())) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.errorCode").value(10001)); + } + + // ========== 주간 목표 ========== + + @Test + void memberCanSetAndClearWeeklyGoalAndNegativeGoalIsRejected() throws Exception { + User host = saveUser("방장", "mgmt-goal-host"); + authenticate(host.getId()); + Long roomId = createRoom("목표 테스트방"); + + // weeklyGoal=20 설정 → 응답에 20 포함 + mockMvc.perform(put("/api/study-room/{roomId}/members/me/goal", roomId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomGoalUpdateRequest(20)))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.weeklyGoal").value(20)); + + // weeklyGoal=0 설정 → null로 초기화, HTTP 200 + mockMvc.perform(put("/api/study-room/{roomId}/members/me/goal", roomId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomGoalUpdateRequest(0)))) + .andExpect(status().isOk()); + + // weeklyGoal=-1 → 유효성 오류 (10016) + mockMvc.perform(put("/api/study-room/{roomId}/members/me/goal", roomId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomGoalUpdateRequest(-1)))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value(10016)); + } + + // ========== 헬퍼 ========== + + private User saveUser(String name, String emailPrefix) { + return userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", name, emailPrefix)); + } + + private void authenticate(Long userId) { + authenticatedUserId = userId; + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken( + userId, null, List.of(new SimpleGrantedAuthority("ROLE_MEMBER")) + ) + ); + } + + private RequestPostProcessor auth() { + return authentication(new UsernamePasswordAuthenticationToken( + authenticatedUserId, null, List.of(new SimpleGrantedAuthority("ROLE_MEMBER")) + )); + } + + private Long createRoom(String name) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomCreateRequest(name)))) + .andExpect(status().isCreated()) + .andReturn(); + return readLong(result, "$.data.roomId"); + } + + private String issueInviteCode(Long roomId) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room/{roomId}/invite", roomId).with(auth())) + .andExpect(status().isOk()) + .andReturn(); + return JsonPath.read(result.getResponse().getContentAsString(), "$.data.code"); + } + + private void join(String code) throws Exception { + mockMvc.perform(post("/api/study-room/join") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomJoinRequest(code)))) + .andExpect(status().isOk()); + } + + private Long readLong(MvcResult result, String path) throws Exception { + Number value = JsonPath.read(result.getResponse().getContentAsString(), path); + return value.longValue(); + } +} diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemApiTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemApiTest.java new file mode 100644 index 00000000..aa1dbb8c --- /dev/null +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemApiTest.java @@ -0,0 +1,243 @@ +package com.aisip.OnO.backend.studyroom.integration; + +import com.aisip.OnO.backend.problem.dto.ProblemRegisterDto; +import com.aisip.OnO.backend.problem.entity.Problem; +import com.aisip.OnO.backend.problem.repository.ProblemRepository; +import com.aisip.OnO.backend.problemsolve.repository.ProblemSolveRepository; +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.repository.UserRepository; +import com.aisip.OnO.backend.util.RandomUserGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +@ActiveProfiles("test") +class StudyRoomSharedProblemApiTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ProblemRepository problemRepository; + + @Autowired + private ProblemSolveRepository problemSolveRepository; + + private Long authenticatedUserId; + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void nonMemberCannotGetSharedProblems() throws Exception { + User host = saveUser("방장", "shared-nonmember-host"); + User outsider = saveUser("외부인", "shared-nonmember-outsider"); + authenticate(host.getId()); + Long roomId = createRoom("공유 문제 접근 테스트방"); + + // 외부인이 공유 문제 목록 조회 → 403 (10002) + authenticate(outsider.getId()); + mockMvc.perform(get("/api/study-room/{roomId}/shared-problems", roomId).with(auth())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(10002)); + } + + @Test + void commentTooLongIsRejected() throws Exception { + User host = saveUser("방장", "shared-comment-host"); + authenticate(host.getId()); + Long roomId = createRoom("댓글 길이 테스트방"); + Problem problem = createProblem(host.getId()); + + // 101자 댓글 → 유효성 오류 (10016) + String tooLongComment = "a".repeat(101); + mockMvc.perform(post("/api/study-room/{roomId}/shared-problems", roomId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new SharedProblemCreateRequest(problem.getId(), tooLongComment)))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value(10016)); + } + + @Test + void nonSharerMemberCannotDeleteSharedProblem() throws Exception { + User host = saveUser("방장", "shared-delete-perm-host"); + User member = saveUser("멤버", "shared-delete-perm-member"); + authenticate(host.getId()); + Long roomId = createRoom("삭제 권한 테스트방"); + String code = issueInviteCode(roomId); + Problem problem = createProblem(host.getId()); + authenticate(member.getId()); + join(code); + + // host가 문제 공유 + authenticate(host.getId()); + Long sharedProblemId = shareProblem(roomId, problem.getId(), "공유 댓글"); + + // member가 host 공유 문제 삭제 시도 → 403 (10002) + authenticate(member.getId()); + mockMvc.perform(delete("/api/study-room/{roomId}/shared-problems/{sharedProblemId}", roomId, sharedProblemId) + .with(auth())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(10002)); + } + + @Test + void sharerCanDeleteOwnSharedProblem() throws Exception { + User host = saveUser("방장", "shared-delete-own-host"); + authenticate(host.getId()); + Long roomId = createRoom("본인 삭제 테스트방"); + Problem problem = createProblem(host.getId()); + + // host가 공유 후 본인 삭제 + Long sharedProblemId = shareProblem(roomId, problem.getId(), "내 공유 문제"); + + mockMvc.perform(delete("/api/study-room/{roomId}/shared-problems/{sharedProblemId}", roomId, sharedProblemId) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data").value("공유 문제 삭제가 완료되었습니다.")); + + // 삭제 후 목록 조회 → 빈 리스트 + MvcResult listResult = mockMvc.perform(get("/api/study-room/{roomId}/shared-problems", roomId).with(auth())) + .andExpect(status().isOk()) + .andReturn(); + List content = JsonPath.read(listResult.getResponse().getContentAsString(), "$.data.content"); + assertThat(content).isEmpty(); + } + + @Test + void sharedProblemPaginationWithCursor() throws Exception { + User host = saveUser("방장", "shared-pagination-host"); + authenticate(host.getId()); + Long roomId = createRoom("공유 문제 페이지네이션 테스트방"); + + // 5개 문제 공유 + for (int i = 0; i < 5; i++) { + Problem problem = createProblem(host.getId()); + shareProblem(roomId, problem.getId(), "댓글 " + i); + } + + // 첫 번째 페이지: size=3 → 3개, hasNext=true + MvcResult firstResult = mockMvc.perform(get("/api/study-room/{roomId}/shared-problems", roomId) + .param("size", "3") + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.hasNext").value(true)) + .andReturn(); + + String firstBody = firstResult.getResponse().getContentAsString(); + List firstContent = JsonPath.read(firstBody, "$.data.content"); + assertThat(firstContent).hasSize(3); + Number nextCursor = JsonPath.read(firstBody, "$.data.nextCursor"); + + // 두 번째 페이지: cursor=nextCursor → 2개, hasNext=false + MvcResult secondResult = mockMvc.perform(get("/api/study-room/{roomId}/shared-problems", roomId) + .param("size", "3") + .param("cursor", nextCursor.toString()) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.hasNext").value(false)) + .andReturn(); + + String secondBody = secondResult.getResponse().getContentAsString(); + List secondContent = JsonPath.read(secondBody, "$.data.content"); + assertThat(secondContent).hasSize(2); + } + + // ========== 헬퍼 ========== + + private User saveUser(String name, String emailPrefix) { + return userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", name, emailPrefix)); + } + + private void authenticate(Long userId) { + authenticatedUserId = userId; + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken( + userId, null, List.of(new SimpleGrantedAuthority("ROLE_MEMBER")) + ) + ); + } + + private RequestPostProcessor auth() { + return authentication(new UsernamePasswordAuthenticationToken( + authenticatedUserId, null, List.of(new SimpleGrantedAuthority("ROLE_MEMBER")) + )); + } + + private Long createRoom(String name) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomCreateRequest(name)))) + .andExpect(status().isCreated()) + .andReturn(); + Number value = JsonPath.read(result.getResponse().getContentAsString(), "$.data.roomId"); + return value.longValue(); + } + + private String issueInviteCode(Long roomId) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room/{roomId}/invite", roomId).with(auth())) + .andExpect(status().isOk()) + .andReturn(); + return JsonPath.read(result.getResponse().getContentAsString(), "$.data.code"); + } + + private void join(String code) throws Exception { + mockMvc.perform(post("/api/study-room/join") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomJoinRequest(code)))) + .andExpect(status().isOk()); + } + + private Problem createProblem(Long userId) { + return problemRepository.save(Problem.from( + new ProblemRegisterDto(null, "memo", "ref", null, LocalDateTime.now()), + userId + )); + } + + private Long shareProblem(Long roomId, Long problemId, String comment) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room/{roomId}/shared-problems", roomId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new SharedProblemCreateRequest(problemId, comment)))) + .andExpect(status().isCreated()) + .andReturn(); + Number value = JsonPath.read(result.getResponse().getContentAsString(), "$.data.sharedProblemId"); + return value.longValue(); + } +} diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomWeeklyReportApiTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomWeeklyReportApiTest.java new file mode 100644 index 00000000..1ed35a43 --- /dev/null +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomWeeklyReportApiTest.java @@ -0,0 +1,218 @@ +package com.aisip.OnO.backend.studyroom.integration; + +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.studyroom.entity.StudyRoom; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomWeeklyReport; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomRepository; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomWeeklyReportRepository; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.repository.UserRepository; +import com.aisip.OnO.backend.util.RandomUserGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + +import java.time.LocalDate; +import java.util.List; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +@ActiveProfiles("test") +class StudyRoomWeeklyReportApiTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private UserRepository userRepository; + + @Autowired + private StudyRoomRepository roomRepository; + + @Autowired + private StudyRoomWeeklyReportRepository reportRepository; + + private Long authenticatedUserId; + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void nonMemberCannotGetWeeklyReports() throws Exception { + User host = saveUser("방장", "report-nonmember-host"); + User outsider = saveUser("외부", "report-nonmember-outsider"); + authenticate(host.getId()); + Long roomId = createRoom("리포트 권한 테스트방"); + + authenticate(outsider.getId()); + mockMvc.perform(get("/api/study-room/{roomId}/weekly-reports", roomId) + .with(auth())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(10002)); + } + + @Test + void memberGetsEmptyListWhenNoReportsExist() throws Exception { + User host = saveUser("방장", "report-empty-host"); + authenticate(host.getId()); + Long roomId = createRoom("빈 리포트방"); + + mockMvc.perform(get("/api/study-room/{roomId}/weekly-reports", roomId) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data.length()").value(0)); + } + + @Test + void memberCanGetReportAndIsReadStatusIsCorrect() throws Exception { + User host = saveUser("방장", "report-read-host"); + User member = saveUser("멤버", "report-read-member"); + authenticate(host.getId()); + Long roomId = createRoom("리포트 읽음 테스트방"); + String inviteCode = issueInviteCode(roomId); + authenticate(member.getId()); + join(inviteCode); + + StudyRoom room = roomRepository.findById(roomId).orElseThrow(); + StudyRoomWeeklyReport report = reportRepository.save(StudyRoomWeeklyReport.create( + room, + LocalDate.now().minusDays(7), + LocalDate.now().minusDays(1), + "방장", 5, + "방장", 3, + 5, 0, + "이번 주도 수고하셨습니다!" + )); + + // 읽기 전: isRead=false + authenticate(member.getId()); + mockMvc.perform(get("/api/study-room/{roomId}/weekly-reports", roomId) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data[0].isRead").value(false)); + + // 읽음 처리: 응답에서 isRead=true + mockMvc.perform(patch("/api/study-room/{roomId}/weekly-reports/{reportId}/read", roomId, report.getId()) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.isRead").value(true)); + + // 다시 조회: isRead=true + mockMvc.perform(get("/api/study-room/{roomId}/weekly-reports", roomId) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data[0].isRead").value(true)); + } + + @Test + void markReadOnNonExistentReportThrowsNotFound() throws Exception { + User host = saveUser("방장", "report-notfound-host"); + User member = saveUser("멤버", "report-notfound-member"); + authenticate(host.getId()); + Long roomId = createRoom("리포트 없음 테스트방"); + String inviteCode = issueInviteCode(roomId); + authenticate(member.getId()); + join(inviteCode); + + mockMvc.perform(patch("/api/study-room/{roomId}/weekly-reports/{reportId}/read", roomId, 9999999L) + .with(auth())) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.errorCode").value(10014)); + } + + @Test + void nonMemberCannotMarkReportRead() throws Exception { + User host = saveUser("방장", "report-forbid-host"); + User outsider = saveUser("외부", "report-forbid-outsider"); + authenticate(host.getId()); + Long roomId = createRoom("리포트 금지 테스트방"); + + StudyRoom room = roomRepository.findById(roomId).orElseThrow(); + StudyRoomWeeklyReport report = reportRepository.save(StudyRoomWeeklyReport.create( + room, + LocalDate.now().minusDays(7), + LocalDate.now().minusDays(1), + "방장", 5, + "방장", 3, + 5, 0, + "이번 주도 수고하셨습니다!" + )); + + authenticate(outsider.getId()); + mockMvc.perform(patch("/api/study-room/{roomId}/weekly-reports/{reportId}/read", roomId, report.getId()) + .with(auth())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(10002)); + } + + // ========== 헬퍼 ========== + + private User saveUser(String name, String emailPrefix) { + return userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", name, emailPrefix)); + } + + private void authenticate(Long userId) { + authenticatedUserId = userId; + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(userId, null, List.of(new SimpleGrantedAuthority("ROLE_MEMBER"))) + ); + } + + private RequestPostProcessor auth() { + return authentication(new UsernamePasswordAuthenticationToken( + authenticatedUserId, null, List.of(new SimpleGrantedAuthority("ROLE_MEMBER")) + )); + } + + private Long createRoom(String name) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomCreateRequest(name)))) + .andExpect(status().isCreated()) + .andReturn(); + Number value = JsonPath.read(result.getResponse().getContentAsString(), "$.data.roomId"); + return value.longValue(); + } + + private String issueInviteCode(Long roomId) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room/{roomId}/invite", roomId) + .with(auth())) + .andExpect(status().isOk()) + .andReturn(); + return JsonPath.read(result.getResponse().getContentAsString(), "$.data.code"); + } + + private void join(String inviteCode) throws Exception { + mockMvc.perform(post("/api/study-room/join") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomJoinRequest(inviteCode)))) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudySessionApiTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudySessionApiTest.java new file mode 100644 index 00000000..d3dfe068 --- /dev/null +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudySessionApiTest.java @@ -0,0 +1,138 @@ +package com.aisip.OnO.backend.studyroom.integration; + +import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.repository.UserRepository; +import com.aisip.OnO.backend.util.RandomUserGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + +import java.util.List; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +@ActiveProfiles("test") +class StudySessionApiTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private UserRepository userRepository; + + private Long authenticatedUserId; + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void nonMemberCannotStartStudySession() throws Exception { + User host = saveUser("방장", "session-nonmember-host"); + User outsider = saveUser("외부", "session-nonmember-outsider"); + authenticate(host.getId()); + Long roomId = createRoom("세션 권한 테스트방"); + + authenticate(outsider.getId()); + mockMvc.perform(post("/api/study-room/{roomId}/sessions", roomId) + .with(auth())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(10002)); + } + + @Test + void cannotEndOtherUsersStudySession() throws Exception { + User host = saveUser("방장", "session-end-host"); + User member = saveUser("멤버", "session-end-member"); + authenticate(host.getId()); + Long roomId = createRoom("세션 종료 테스트방"); + String inviteCode = issueInviteCode(roomId); + authenticate(member.getId()); + join(inviteCode); + + // host가 세션 시작 + authenticate(host.getId()); + MvcResult startResult = mockMvc.perform(post("/api/study-room/{roomId}/sessions", roomId) + .with(auth())) + .andExpect(status().isCreated()) + .andReturn(); + Number sessionIdValue = JsonPath.read(startResult.getResponse().getContentAsString(), "$.data.sessionId"); + Long sessionId = sessionIdValue.longValue(); + + // member가 host 세션 종료 시도 → SESSION_NOT_FOUND (10012): findByIdAndRoomIdAndUserId 미일치 + authenticate(member.getId()); + mockMvc.perform(patch("/api/study-room/{roomId}/sessions/{sessionId}/end", roomId, sessionId) + .with(auth())) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.errorCode").value(10012)); + } + + // ========== 헬퍼 ========== + + private User saveUser(String name, String emailPrefix) { + return userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", name, emailPrefix)); + } + + private void authenticate(Long userId) { + authenticatedUserId = userId; + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(userId, null, List.of(new SimpleGrantedAuthority("ROLE_MEMBER"))) + ); + } + + private RequestPostProcessor auth() { + return authentication(new UsernamePasswordAuthenticationToken( + authenticatedUserId, null, List.of(new SimpleGrantedAuthority("ROLE_MEMBER")) + )); + } + + private Long createRoom(String name) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomCreateRequest(name)))) + .andExpect(status().isCreated()) + .andReturn(); + Number value = JsonPath.read(result.getResponse().getContentAsString(), "$.data.roomId"); + return value.longValue(); + } + + private String issueInviteCode(Long roomId) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-room/{roomId}/invite", roomId) + .with(auth())) + .andExpect(status().isOk()) + .andReturn(); + return JsonPath.read(result.getResponse().getContentAsString(), "$.data.code"); + } + + private void join(String inviteCode) throws Exception { + mockMvc.perform(post("/api/study-room/join") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new StudyRoomJoinRequest(inviteCode)))) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedServiceUnitTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedServiceUnitTest.java new file mode 100644 index 00000000..93601b21 --- /dev/null +++ b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedServiceUnitTest.java @@ -0,0 +1,128 @@ +package com.aisip.OnO.backend.studyroom.service; + +import com.aisip.OnO.backend.common.emoji.CustomEmojiValidator; +import com.aisip.OnO.backend.studyroom.entity.*; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomFeedReactionRepository; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomFeedRepository; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomMemberRepository; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.repository.UserRepository; +import com.aisip.OnO.backend.util.RandomUserGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.springframework.test.util.ReflectionTestUtils.setField; + +@ExtendWith(MockitoExtension.class) +class StudyRoomFeedServiceUnitTest { + + @Mock + private StudyRoomAccessService accessService; + + @Mock + private StudyRoomMemberRepository memberRepository; + + @Mock + private StudyRoomFeedRepository feedRepository; + + @Mock + private StudyRoomFeedReactionRepository reactionRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private StudyRoomReactionService reactionService; + + @Mock + private CustomEmojiValidator customEmojiValidator; + + private StudyRoomFeedService feedService; + + private User user; + private StudyRoomMember membership; + private StudyRoom room; + + @BeforeEach + void setUp() { + user = RandomUserGenerator.createRandomUser("GOOGLE", "테스트", "feed-unit-test"); + setField(user, "id", 1L); + + room = StudyRoom.create("테스트방", 1L); + setField(room, "id", 10L); + + membership = StudyRoomMember.create(user, StudyRoomMemberRole.MEMBER); + membership.updateRoom(room); + + feedService = new StudyRoomFeedService( + accessService, memberRepository, feedRepository, + reactionRepository, userRepository, new ObjectMapper(), + reactionService, customEmojiValidator + ); + + given(userRepository.findById(1L)).willReturn(Optional.of(user)); + given(memberRepository.findAllWithRoomByUserId(1L)).willReturn(List.of(membership)); + } + + @Test + void problemRegisteredAccumulatesCountOnSameDayFeed() { + // 오늘 이미 count=2인 피드가 존재 + StudyRoomFeed existingFeed = StudyRoomFeed.create( + room, user, StudyRoomFeedEventType.PROBLEM_REGISTERED, "{\"count\":2}"); + + given(feedRepository.findTopByRoomIdAndUserIdAndEventTypeAndCreatedAtBetweenOrderByCreatedAtDesc( + any(), any(), any(StudyRoomFeedEventType.class), any(LocalDateTime.class), any(LocalDateTime.class))) + .willReturn(Optional.of(existingFeed)); + + // count=3 추가 → 기존 count(2) + 신규 count(3) = 5 + feedService.createFeedsForUserRooms(1L, StudyRoomFeedEventType.PROBLEM_REGISTERED, Map.of("count", 3)); + + // 새 피드를 저장해서는 안 됨 + verify(feedRepository, never()).save(any()); + // 기존 피드의 count가 5로 업데이트되어야 함 + assertThat(existingFeed.getMetadataJson()).contains("\"count\":5"); + } + + @Test + void problemRegisteredCreatesNewFeedWhenNoSameDayFeedExists() { + // 오늘 동일 유형의 피드가 없음 + given(feedRepository.findTopByRoomIdAndUserIdAndEventTypeAndCreatedAtBetweenOrderByCreatedAtDesc( + any(), any(), any(StudyRoomFeedEventType.class), any(LocalDateTime.class), any(LocalDateTime.class))) + .willReturn(Optional.empty()); + given(feedRepository.save(any())).willAnswer(inv -> inv.getArgument(0)); + + feedService.createFeedsForUserRooms(1L, StudyRoomFeedEventType.PROBLEM_REGISTERED, Map.of("count", 1)); + + // 새 피드가 저장되어야 함 + verify(feedRepository).save(any(StudyRoomFeed.class)); + } + + @Test + void nonProblemRegisteredEventAlwaysCreatesNewFeed() { + given(feedRepository.save(any())).willAnswer(inv -> inv.getArgument(0)); + + // SESSION_STARTED는 당일 중복 체크 없이 항상 새 피드 생성 + feedService.createFeedsForUserRooms(1L, StudyRoomFeedEventType.SESSION_STARTED, Map.of()); + + // 새 피드가 저장되어야 함 + verify(feedRepository).save(any(StudyRoomFeed.class)); + // 당일 기존 피드 조회 쿼리는 호출되면 안 됨 + verify(feedRepository, never()) + .findTopByRoomIdAndUserIdAndEventTypeAndCreatedAtBetweenOrderByCreatedAtDesc( + any(), any(), any(), any(), any()); + } +} From 40521bb41ac7c8a0fe091660b1a37a008447d60a Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sun, 28 Jun 2026 23:38:42 +0900 Subject: [PATCH 35/67] =?UTF-8?q?[Fix]=20=EA=B3=B5=EC=9C=A0=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EB=8C=93=EA=B8=80=20=EC=A0=95=EC=B1=85=20=EB=B3=B4?= =?UTF-8?q?=EA=B0=95=20-=20300=EC=9E=90=20=EC=A0=9C=ED=95=9C=20=EC=9D=BD?= =?UTF-8?q?=EA=B8=B0=20=EC=A0=84=EC=9A=A9=20=ED=9A=8C=EA=B7=80=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/StudyRoomErrorCase.java | 4 +- .../StudyRoomSharedProblemCommentService.java | 8 +-- .../StudyRoomSharedProblemApiTest.java | 50 +++++++++++++++++++ .../StudyRoomSharedProblemCommentApiTest.java | 8 +-- 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/exception/StudyRoomErrorCase.java b/src/main/java/com/aisip/OnO/backend/studyroom/exception/StudyRoomErrorCase.java index 1aa93e76..ffcbec6e 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/exception/StudyRoomErrorCase.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/exception/StudyRoomErrorCase.java @@ -24,7 +24,9 @@ public enum StudyRoomErrorCase implements ErrorCase { REPORT_NOT_FOUND(404, 10014, "주간 리포트를 찾을 수 없습니다."), INVALID_REACTION_EMOJI(400, 10015, "허용되지 않은 반응입니다."), INVALID_STUDY_ROOM_REQUEST(400, 10016, "스터디룸 요청 값이 올바르지 않습니다."), - SHARED_PROBLEM_COMMENT_NOT_FOUND(404, 10017, "공유 문제 댓글을 찾을 수 없습니다."); + SHARED_PROBLEM_COMMENT_NOT_FOUND(404, 10017, "공유 문제 댓글을 찾을 수 없습니다."), + INVALID_SHARED_PROBLEM_COMMENT(400, 10018, "공유 문제 댓글 내용이 올바르지 않습니다."), + SHARED_PROBLEM_COMMENT_FORBIDDEN(403, 10019, "공유 문제 댓글에 대한 권한이 없습니다."); private final Integer httpStatusCode; private final Integer errorCode; diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemCommentService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemCommentService.java index e812b860..0f5a7cea 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemCommentService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemCommentService.java @@ -25,7 +25,7 @@ @RequiredArgsConstructor public class StudyRoomSharedProblemCommentService { - private static final int MAX_COMMENT_LENGTH = 1000; + private static final int MAX_COMMENT_LENGTH = 300; private final StudyRoomAccessService accessService; private final StudyRoomSharedProblemRepository sharedProblemRepository; @@ -69,7 +69,7 @@ public SharedProblemCommentResponse updateComment(Long roomId, Long sharedProble accessService.validateMember(roomId, userId); StudyRoomSharedProblemComment comment = getCommentOrThrow(roomId, sharedProblemId, commentId); if (!comment.getAuthor().getId().equals(userId)) { - throw new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_FORBIDDEN); + throw new ApplicationException(StudyRoomErrorCase.SHARED_PROBLEM_COMMENT_FORBIDDEN); } comment.updateContent(validateContent(request)); return toResponse(comment, userId); @@ -82,7 +82,7 @@ public void deleteComment(Long roomId, Long sharedProblemId, Long commentId, Lon boolean mine = comment.getAuthor().getId().equals(userId); boolean host = member.getRole() == StudyRoomMemberRole.HOST; if (!mine && !host) { - throw new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_FORBIDDEN); + throw new ApplicationException(StudyRoomErrorCase.SHARED_PROBLEM_COMMENT_FORBIDDEN); } commentRepository.delete(comment); } @@ -100,7 +100,7 @@ private StudyRoomSharedProblemComment getCommentOrThrow(Long roomId, Long shared private String validateContent(SharedProblemCommentRequest request) { String content = request == null ? null : request.content(); if (content == null || content.isBlank() || content.length() > MAX_COMMENT_LENGTH) { - throw new ApplicationException(StudyRoomErrorCase.INVALID_STUDY_ROOM_REQUEST); + throw new ApplicationException(StudyRoomErrorCase.INVALID_SHARED_PROBLEM_COMMENT); } return content.trim(); } diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemApiTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemApiTest.java index aa1dbb8c..f83c5dd5 100644 --- a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemApiTest.java +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemApiTest.java @@ -3,6 +3,9 @@ import com.aisip.OnO.backend.problem.dto.ProblemRegisterDto; import com.aisip.OnO.backend.problem.entity.Problem; import com.aisip.OnO.backend.problem.repository.ProblemRepository; +import com.aisip.OnO.backend.practicenote.dto.PracticeNoteRegisterDto; +import com.aisip.OnO.backend.problemsolve.dto.ProblemSolveRegisterDto; +import com.aisip.OnO.backend.problemsolve.entity.AnswerStatus; import com.aisip.OnO.backend.problemsolve.repository.ProblemSolveRepository; import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; import com.aisip.OnO.backend.user.entity.User; @@ -176,6 +179,53 @@ void sharedProblemPaginationWithCursor() throws Exception { assertThat(secondContent).hasSize(2); } + @Test + void sharedProblemDoesNotAllowOtherMemberToUseOriginalProblemAsOwnStudyData() throws Exception { + User host = saveUser("방장", "shared-readonly-host"); + User member = saveUser("멤버", "shared-readonly-member"); + authenticate(host.getId()); + Long roomId = createRoom("읽기 전용 정책방"); + String code = issueInviteCode(roomId); + Problem hostProblem = createProblem(host.getId()); + shareProblem(roomId, hostProblem.getId(), "읽기 전용 공유 문제"); + + authenticate(member.getId()); + join(code); + + mockMvc.perform(get("/api/problems/{problemId}", hostProblem.getId()) + .with(auth())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(4002)); + + mockMvc.perform(post("/api/problem-solves") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ProblemSolveRegisterDto( + hostProblem.getId(), + LocalDateTime.now(), + AnswerStatus.CORRECT, + "타인 공유 문제 복습 시도", + List.of(), + 60 + )))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(4002)); + + mockMvc.perform(post("/api/practiceNotes") + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new PracticeNoteRegisterDto( + null, + "타인 공유 문제 편입 시도", + List.of(hostProblem.getId()), + null + )))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.errorCode").value(4002)); + + assertThat(problemSolveRepository.countByProblemId(hostProblem.getId())).isZero(); + } + // ========== 헬퍼 ========== private User saveUser(String name, String emailPrefix) { diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemCommentApiTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemCommentApiTest.java index 3990a3e5..358d6f42 100644 --- a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemCommentApiTest.java +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemCommentApiTest.java @@ -117,7 +117,7 @@ void onlyAuthorCanUpdateAndHostCanDeleteOtherMemberComment() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(new SharedProblemCommentRequest("권한 없는 수정")))) .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.errorCode").value(10002)); + .andExpect(jsonPath("$.errorCode").value(10019)); authenticate(host.getId()); mockMvc.perform(delete("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments/{commentId}", @@ -176,14 +176,14 @@ void blankOrTooLongCommentIsRejected() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(new SharedProblemCommentRequest(" ")))) .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.errorCode").value(10016)); + .andExpect(jsonPath("$.errorCode").value(10018)); mockMvc.perform(post("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments", roomId, sharedProblemId) .with(auth()) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(new SharedProblemCommentRequest("a".repeat(1001))))) + .content(objectMapper.writeValueAsString(new SharedProblemCommentRequest("a".repeat(301))))) .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.errorCode").value(10016)); + .andExpect(jsonPath("$.errorCode").value(10018)); } private User saveUser(String name, String emailPrefix) { From 1eaf6ccf2745b7fa22c1625cc10df72c213390d9 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Mon, 29 Jun 2026 10:47:09 +0900 Subject: [PATCH 36/67] =?UTF-8?q?[Fix]=20=EA=B3=B5=EC=9C=A0=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EB=8C=93=EA=B8=80=20=EC=9D=91=EB=8B=B5=20=EA=B3=84?= =?UTF-8?q?=EC=95=BD=20=EB=B3=B4=EA=B0=95=20-=20=EB=B0=A9=EC=9E=A5=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EA=B6=8C=ED=95=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=B3=B4=EC=95=88=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/config/SecurityConfig.java | 2 +- .../backend/studyroom/dto/StudyRoomDtos.java | 3 ++- .../StudyRoomSharedProblemCommentService.java | 18 +++++++++++++----- .../StudyRoomSharedProblemCommentApiTest.java | 19 ++++++++++++++++++- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/auth/config/SecurityConfig.java b/src/main/java/com/aisip/OnO/backend/auth/config/SecurityConfig.java index 854d340c..d162df1f 100644 --- a/src/main/java/com/aisip/OnO/backend/auth/config/SecurityConfig.java +++ b/src/main/java/com/aisip/OnO/backend/auth/config/SecurityConfig.java @@ -106,7 +106,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/api/folders/**").hasAnyRole("GUEST", "MEMBER", "ADMIN") .requestMatchers("/api/fileUpload/**").hasAnyRole("GUEST", "MEMBER", "ADMIN") .requestMatchers("/api/practiceNotes/**").hasAnyRole("GUEST", "MEMBER", "ADMIN") - .requestMatchers("/api/study-room/**").hasAnyRole("GUEST", "MEMBER", "ADMIN") + .requestMatchers("/api/study-room/**", "/api/study-rooms/**").hasAnyRole("GUEST", "MEMBER", "ADMIN") .anyRequest().authenticated() ) .formLogin(formLogin -> formLogin diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java index 38c15eae..379b122d 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java @@ -106,7 +106,8 @@ public record SharedProblemReactionToggleResponse(Long sharedProblemId, List new ApplicationException(UserErrorCase.USER_NOT_FOUND)); StudyRoomSharedProblemComment comment = commentRepository.save( StudyRoomSharedProblemComment.create(sharedProblem, user, validateContent(request))); - return toResponse(comment, userId); + return toResponse(comment, userId, false); } @Transactional @@ -72,7 +72,7 @@ public SharedProblemCommentResponse updateComment(Long roomId, Long sharedProble throw new ApplicationException(StudyRoomErrorCase.SHARED_PROBLEM_COMMENT_FORBIDDEN); } comment.updateContent(validateContent(request)); - return toResponse(comment, userId); + return toResponse(comment, userId, true); } @Transactional @@ -106,18 +106,26 @@ private String validateContent(SharedProblemCommentRequest request) { } private SharedProblemCommentResponse toResponse(StudyRoomSharedProblemComment comment, Long userId) { + return toResponse(comment, userId, false); + } + + private SharedProblemCommentResponse toResponse(StudyRoomSharedProblemComment comment, Long userId, boolean forceEdited) { LocalDateTime createdAt = comment.getCreatedAt(); LocalDateTime updatedAt = comment.getUpdatedAt(); + Long authorId = comment.getAuthor().getId(); + boolean mine = authorId.equals(userId); + boolean canDelete = mine || comment.getSharedProblem().getRoom().getHostUserId().equals(userId); return new SharedProblemCommentResponse( comment.getId(), comment.getContent(), - comment.getAuthor().getId(), + authorId, comment.getAuthor().getName(), null, createdAt, updatedAt, - createdAt != null && updatedAt != null && updatedAt.isAfter(createdAt), - comment.getAuthor().getId().equals(userId) + forceEdited || createdAt != null && updatedAt != null && updatedAt.isAfter(createdAt), + mine, + canDelete ); } } diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemCommentApiTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemCommentApiTest.java index 358d6f42..a6c354d3 100644 --- a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemCommentApiTest.java +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemCommentApiTest.java @@ -79,7 +79,9 @@ void memberCanCreateUpdateAndListSharedProblemComments() throws Exception { .andExpect(jsonPath("$.data.content").value("수정된 풀이 의견")) .andExpect(jsonPath("$.data.authorId").value(member.getId())) .andExpect(jsonPath("$.data.authorName").value("멤버")) - .andExpect(jsonPath("$.data.isMine").value(true)); + .andExpect(jsonPath("$.data.isEdited").value(true)) + .andExpect(jsonPath("$.data.isMine").value(true)) + .andExpect(jsonPath("$.data.canDelete").value(true)); MvcResult result = mockMvc.perform(get("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments", roomId, sharedProblemId) @@ -89,6 +91,7 @@ void memberCanCreateUpdateAndListSharedProblemComments() throws Exception { .andExpect(jsonPath("$.data.content[0].commentId").value(commentId)) .andExpect(jsonPath("$.data.content[0].content").value("수정된 풀이 의견")) .andExpect(jsonPath("$.data.content[0].isMine").value(true)) + .andExpect(jsonPath("$.data.content[0].canDelete").value(true)) .andReturn(); List comments = JsonPath.read(result.getResponse().getContentAsString(), "$.data.content"); @@ -111,6 +114,13 @@ void onlyAuthorCanUpdateAndHostCanDeleteOtherMemberComment() throws Exception { authenticate(anotherMember.getId()); join(inviteCode); + mockMvc.perform(get("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments", + roomId, sharedProblemId) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.content[0].isMine").value(false)) + .andExpect(jsonPath("$.data.content[0].canDelete").value(false)); + mockMvc.perform(patch("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments/{commentId}", roomId, sharedProblemId, commentId) .with(auth()) @@ -120,6 +130,13 @@ void onlyAuthorCanUpdateAndHostCanDeleteOtherMemberComment() throws Exception { .andExpect(jsonPath("$.errorCode").value(10019)); authenticate(host.getId()); + mockMvc.perform(get("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments", + roomId, sharedProblemId) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.content[0].isMine").value(false)) + .andExpect(jsonPath("$.data.content[0].canDelete").value(true)); + mockMvc.perform(delete("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments/{commentId}", roomId, sharedProblemId, commentId) .with(auth())) From 8a348a412415e6475bd16b1c5a6cb8ad2c9079c9 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Mon, 29 Jun 2026 10:47:41 +0900 Subject: [PATCH 37/67] =?UTF-8?q?[Fix]=20=EA=B3=B5=EC=9C=A0=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EB=8C=93=EA=B8=80=20=EC=82=AD=EC=A0=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=20=EA=B0=9C=EC=84=A0=20-=20bulk=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20=ED=83=88=ED=87=B4=20=EB=8C=93=EA=B8=80=20=EC=84=A0=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StudyRoomSharedProblemCommentRepository.java | 11 ++++++++++- .../StudyRoomSharedProblemReactionRepository.java | 5 ++++- .../aisip/OnO/backend/user/service/UserService.java | 4 ++++ .../OnO/backend/user/service/UserServiceTest.java | 12 ++++++++---- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemCommentRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemCommentRepository.java index 5526e5db..6f69f809 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemCommentRepository.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemCommentRepository.java @@ -3,6 +3,7 @@ import com.aisip.OnO.backend.studyroom.entity.StudyRoomSharedProblemComment; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -14,6 +15,8 @@ public interface StudyRoomSharedProblemCommentRepository extends JpaRepository findByIdAndSharedProblemIdAndRoomId(@Par @Param("sharedProblemId") Long sharedProblemId, @Param("roomId") Long roomId); - void deleteBySharedProblemId(Long sharedProblemId); + @Modifying + @Query("delete from StudyRoomSharedProblemComment c where c.sharedProblem.id = :sharedProblemId") + void deleteBySharedProblemId(@Param("sharedProblemId") Long sharedProblemId); + + @Modifying + @Query("delete from StudyRoomSharedProblemComment c where c.author.id = :authorId") + void deleteByAuthorId(@Param("authorId") Long authorId); } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemReactionRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemReactionRepository.java index 4b28152f..31d83f99 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemReactionRepository.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemReactionRepository.java @@ -2,6 +2,7 @@ import com.aisip.OnO.backend.studyroom.entity.StudyRoomSharedProblemReaction; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -18,5 +19,7 @@ public interface StudyRoomSharedProblemReactionRepository extends JpaRepository< List findAllBySharedProblemId(Long sharedProblemId); - void deleteBySharedProblemId(Long sharedProblemId); + @Modifying + @Query("delete from StudyRoomSharedProblemReaction r where r.sharedProblem.id = :sharedProblemId") + void deleteBySharedProblemId(@Param("sharedProblemId") Long sharedProblemId); } diff --git a/src/main/java/com/aisip/OnO/backend/user/service/UserService.java b/src/main/java/com/aisip/OnO/backend/user/service/UserService.java index 34e89837..311ed93a 100644 --- a/src/main/java/com/aisip/OnO/backend/user/service/UserService.java +++ b/src/main/java/com/aisip/OnO/backend/user/service/UserService.java @@ -4,6 +4,7 @@ import com.aisip.OnO.backend.folder.service.FolderService; import com.aisip.OnO.backend.practicenote.service.PracticeNoteService; import com.aisip.OnO.backend.problem.service.ProblemService; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomSharedProblemCommentRepository; import com.aisip.OnO.backend.user.dto.UserRegisterDto; import com.aisip.OnO.backend.user.dto.UserResponseDto; import com.aisip.OnO.backend.user.entity.User; @@ -42,6 +43,8 @@ public class UserService { private final PracticeNoteService practiceNoteService; + private final StudyRoomSharedProblemCommentRepository sharedProblemCommentRepository; + private final DiscordWebhookNotificationService discordWebhookNotificationService; private User findUserEntity(Long userId){ @@ -143,6 +146,7 @@ public void updateUser(Long userId, UserRegisterDto userRegisterDto) { @Transactional public void deleteUserById(Long userId) { User user = findUserEntity(userId); + sharedProblemCommentRepository.deleteByAuthorId(userId); practiceNoteService.deleteAllPracticesByUser(userId); problemService.deleteAllUserProblems(userId); folderService.deleteAllUserFolders(userId); diff --git a/src/test/java/com/aisip/OnO/backend/user/service/UserServiceTest.java b/src/test/java/com/aisip/OnO/backend/user/service/UserServiceTest.java index 373c1e45..b3e0f2b8 100644 --- a/src/test/java/com/aisip/OnO/backend/user/service/UserServiceTest.java +++ b/src/test/java/com/aisip/OnO/backend/user/service/UserServiceTest.java @@ -4,6 +4,7 @@ import com.aisip.OnO.backend.folder.service.FolderService; import com.aisip.OnO.backend.practicenote.service.PracticeNoteService; import com.aisip.OnO.backend.problem.service.ProblemService; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomSharedProblemCommentRepository; import com.aisip.OnO.backend.user.dto.UserRegisterDto; import com.aisip.OnO.backend.user.dto.UserResponseDto; import com.aisip.OnO.backend.user.entity.User; @@ -41,6 +42,8 @@ class UserServiceTest { @Mock private PracticeNoteService practiceNoteService; @Mock + private StudyRoomSharedProblemCommentRepository sharedProblemCommentRepository; + @Mock private DiscordWebhookNotificationService discordWebhookNotificationService; @InjectMocks @@ -122,8 +125,8 @@ void registerGuestUser() { assertThat(response.email()).contains("guest"); verify(userRepository, times(1)).save(any(User.class)); - verify(folderService, times(1)).initializeDefaultFoldersIfAbsent(any(Long.class)); - verify(practiceNoteService, times(1)).registerDefaultPractice(any(Long.class)); + verify(folderService, times(1)).initializeDefaultFoldersIfAbsent(any()); + verify(practiceNoteService, times(1)).registerDefaultPractice(any()); } @Test @@ -145,8 +148,8 @@ void registerMemberUser() { // save()가 반드시 한 번 호출되어야 함 verify(userRepository, times(1)).save(any(User.class)); - verify(folderService, times(1)).initializeDefaultFoldersIfAbsent(any(Long.class)); - verify(practiceNoteService, times(1)).registerDefaultPractice(any(Long.class)); + verify(folderService, times(1)).initializeDefaultFoldersIfAbsent(any()); + verify(practiceNoteService, times(1)).registerDefaultPractice(any()); } @Test @@ -247,6 +250,7 @@ void deleteUserById() { // Then assertThat(user.getIdentifier()).startsWith("deleted:" + userId + ":"); + verify(sharedProblemCommentRepository, times(1)).deleteByAuthorId(userId); verify(practiceNoteService, times(1)).deleteAllPracticesByUser(userId); verify(problemService, times(1)).deleteAllUserProblems(userId); verify(folderService, times(1)).deleteAllUserFolders(userId); From 6122bec37039be5c0888d7da126d3a62e55a640d Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Mon, 29 Jun 2026 11:22:52 +0900 Subject: [PATCH 38/67] =?UTF-8?q?[Feat]=20=EC=86=8C=EC=85=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=EB=94=94=EB=A3=B8=20=EC=9D=91=EB=8B=B5=20=EB=B3=B4?= =?UTF-8?q?=EA=B0=95=20-=20=EB=8C=93=EA=B8=80=20=EB=B0=98=EC=9D=91=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20API?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StudyRoomSharedProblemController.java | 8 + .../backend/studyroom/dto/StudyRoomDtos.java | 15 +- ...StudyRoomSharedProblemCommentReaction.java | 43 ++++++ ...haredProblemCommentReactionRepository.java | 38 +++++ ...udyRoomSharedProblemCommentRepository.java | 9 ++ .../service/StudyRoomFeedService.java | 1 + .../studyroom/service/StudyRoomMapper.java | 1 + .../service/StudyRoomReactionService.java | 5 + .../StudyRoomSharedProblemCommentService.java | 55 ++++++- .../StudyRoomSharedProblemService.java | 40 ++++- .../user/controller/UserController.java | 14 ++ .../OnO/backend/user/dto/UserResponseDto.java | 2 + .../aisip/OnO/backend/user/entity/User.java | 7 + .../OnO/backend/user/service/UserService.java | 145 ++++++++++++++++++ ...mment_reactions_and_user_profile_image.sql | 18 +++ .../StudyRoomSharedProblemApiTest.java | 67 +++++++- .../StudyRoomSharedProblemCommentApiTest.java | 55 ++++++- .../user/controller/UserControllerTest.java | 1 + .../backend/user/service/UserServiceTest.java | 59 +++++++ 19 files changed, 560 insertions(+), 23 deletions(-) create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblemCommentReaction.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemCommentReactionRepository.java create mode 100644 src/main/resources/db/migration/V11__add_comment_reactions_and_user_profile_image.sql diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomSharedProblemController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomSharedProblemController.java index 4dcba9b4..20d95d5e 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomSharedProblemController.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomSharedProblemController.java @@ -63,6 +63,14 @@ public CommonResponse updateComment(@PathVariable( return CommonResponse.success(commentService.updateComment(roomId, sharedProblemId, commentId, currentUserId(), request)); } + @PostMapping("/{sharedProblemId}/comments/{commentId}/reactions") + public CommonResponse toggleCommentReaction(@PathVariable("roomId") Long roomId, + @PathVariable("sharedProblemId") Long sharedProblemId, + @PathVariable("commentId") Long commentId, + @RequestBody ReactionToggleRequest request) { + return CommonResponse.success(commentService.toggleReaction(roomId, sharedProblemId, commentId, currentUserId(), request)); + } + @DeleteMapping("/{sharedProblemId}/comments/{commentId}") public CommonResponse deleteComment(@PathVariable("roomId") Long roomId, @PathVariable("sharedProblemId") Long sharedProblemId, diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java index 379b122d..87dac932 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java @@ -44,7 +44,8 @@ public record StudyRoomDetailResponse(Long roomId, String name, Long hostUserId, int memberCount, List members) { } - public record StudyRoomMemberResponse(Long userId, String name, Long totalStudyLevel, int currentStreak, + public record StudyRoomMemberResponse(Long userId, String name, String profileImageUrl, + Long totalStudyLevel, int currentStreak, int weeklyProblemCount, int weeklyPracticeCount, Integer weeklyGoal, Integer goalProgress, int todayPracticeCount, boolean practicedToday) { @@ -65,7 +66,7 @@ public record PageResponse(List content, int page, int size, long totalEle public record ReactionResponse(String emoji, long count, boolean reactedByMe) { } - public record FeedItemResponse(Long feedId, Long userId, String userName, String eventType, + public record FeedItemResponse(Long feedId, Long userId, String userName, String userProfileImageUrl, String eventType, Map metadata, LocalDateTime createdAt, List reactions) { } @@ -96,18 +97,22 @@ public record StudySessionEndResponse(Long sessionId, LocalDateTime startedAt, L } public record SharedProblemResponse(Long sharedProblemId, Long sharedByUserId, String sharedByName, - Long problemId, String problemImageUrl, String reference, - String comment, LocalDateTime sharedAt, + String sharedByProfileImageUrl, + Long problemId, String problemImageUrl, List problemImageUrls, String reference, + String comment, long commentCount, LocalDateTime sharedAt, List reactions) { } public record SharedProblemReactionToggleResponse(Long sharedProblemId, List reactions) { } + public record SharedProblemCommentReactionToggleResponse(Long commentId, List reactions) { + } + public record SharedProblemCommentResponse(Long commentId, String content, Long authorId, String authorName, String authorProfileImageUrl, LocalDateTime createdAt, LocalDateTime updatedAt, boolean isEdited, boolean isMine, - boolean canDelete) { + boolean canDelete, List reactions) { } public record WeeklyReportResponse(Long reportId, LocalDate weekStart, LocalDate weekEnd, diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblemCommentReaction.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblemCommentReaction.java new file mode 100644 index 00000000..d540c983 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomSharedProblemCommentReaction.java @@ -0,0 +1,43 @@ +package com.aisip.OnO.backend.studyroom.entity; + +import com.aisip.OnO.backend.common.entity.BaseEntity; +import com.aisip.OnO.backend.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +@Getter +@Entity +@Builder(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "study_room_shared_problem_comment_reaction", + uniqueConstraints = @UniqueConstraint(name = "uk_shared_problem_comment_reaction", columnNames = {"comment_id", "user_id", "emoji"}), + indexes = { + @Index(name = "idx_shared_problem_comment_reaction_comment", columnList = "comment_id"), + @Index(name = "idx_shared_problem_comment_reaction_user", columnList = "user_id") + }) +public class StudyRoomSharedProblemCommentReaction extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "comment_id", nullable = false) + private StudyRoomSharedProblemComment comment; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(nullable = false, length = 80) + private String emoji; + + public static StudyRoomSharedProblemCommentReaction create(StudyRoomSharedProblemComment comment, User user, String emoji) { + return StudyRoomSharedProblemCommentReaction.builder() + .comment(comment) + .user(user) + .emoji(emoji) + .build(); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemCommentReactionRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemCommentReactionRepository.java new file mode 100644 index 00000000..a89fb012 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemCommentReactionRepository.java @@ -0,0 +1,38 @@ +package com.aisip.OnO.backend.studyroom.repository; + +import com.aisip.OnO.backend.studyroom.entity.StudyRoomSharedProblemCommentReaction; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +public interface StudyRoomSharedProblemCommentReactionRepository extends JpaRepository { + + Optional findByCommentIdAndUserIdAndEmoji(Long commentId, Long userId, String emoji); + + @Query("select r from StudyRoomSharedProblemCommentReaction r where r.comment.id = :commentId") + List findAllByCommentId(@Param("commentId") Long commentId); + + @Query("select r from StudyRoomSharedProblemCommentReaction r where r.comment.id in :commentIds") + List findAllByCommentIds(@Param("commentIds") Collection commentIds); + + @Modifying + @Query("delete from StudyRoomSharedProblemCommentReaction r where r.user.id = :userId") + void deleteByUserId(@Param("userId") Long userId); + + @Modifying + @Query("delete from StudyRoomSharedProblemCommentReaction r where r.comment.id = :commentId") + void deleteByCommentId(@Param("commentId") Long commentId); + + @Modifying + @Query("delete from StudyRoomSharedProblemCommentReaction r where r.comment.sharedProblem.id = :sharedProblemId") + void deleteBySharedProblemId(@Param("sharedProblemId") Long sharedProblemId); + + @Modifying + @Query("delete from StudyRoomSharedProblemCommentReaction r where r.comment.author.id = :authorId") + void deleteByCommentAuthorId(@Param("authorId") Long authorId); +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemCommentRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemCommentRepository.java index 6f69f809..66077287 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemCommentRepository.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemCommentRepository.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.Optional; +import java.util.Collection; public interface StudyRoomSharedProblemCommentRepository extends JpaRepository { @@ -40,6 +41,14 @@ Optional findByIdAndSharedProblemIdAndRoomId(@Par @Param("sharedProblemId") Long sharedProblemId, @Param("roomId") Long roomId); + @Query(""" + select c.sharedProblem.id, count(c.id) + from StudyRoomSharedProblemComment c + where c.sharedProblem.id in :sharedProblemIds + group by c.sharedProblem.id + """) + List countBySharedProblemIds(@Param("sharedProblemIds") Collection sharedProblemIds); + @Modifying @Query("delete from StudyRoomSharedProblemComment c where c.sharedProblem.id = :sharedProblemId") void deleteBySharedProblemId(@Param("sharedProblemId") Long sharedProblemId); diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedService.java index 71c93cbf..5f1642f7 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedService.java @@ -126,6 +126,7 @@ private FeedItemResponse toFeedItem(StudyRoomFeed feed, List summarizeSharedProblemReactions(Collection reaction.getUser().getId()); } + public List summarizeSharedProblemCommentReactions(Collection reactions, Long userId) { + return summarize(reactions, userId, StudyRoomSharedProblemCommentReaction::getEmoji, reaction -> reaction.getUser().getId()); + } + private List summarize(Collection reactions, Long userId, Function emojiGetter, Function userIdGetter) { diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemCommentService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemCommentService.java index d296767c..c0a5df9f 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemCommentService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemCommentService.java @@ -1,5 +1,6 @@ package com.aisip.OnO.backend.studyroom.service; +import com.aisip.OnO.backend.common.emoji.CustomEmojiValidator; import com.aisip.OnO.backend.common.exception.ApplicationException; import com.aisip.OnO.backend.common.response.CursorPageResponse; import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; @@ -7,8 +8,10 @@ import com.aisip.OnO.backend.studyroom.entity.StudyRoomMemberRole; import com.aisip.OnO.backend.studyroom.entity.StudyRoomSharedProblem; import com.aisip.OnO.backend.studyroom.entity.StudyRoomSharedProblemComment; +import com.aisip.OnO.backend.studyroom.entity.StudyRoomSharedProblemCommentReaction; import com.aisip.OnO.backend.studyroom.exception.StudyRoomErrorCase; import com.aisip.OnO.backend.studyroom.repository.StudyRoomSharedProblemCommentRepository; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomSharedProblemCommentReactionRepository; import com.aisip.OnO.backend.studyroom.repository.StudyRoomSharedProblemRepository; import com.aisip.OnO.backend.user.entity.User; import com.aisip.OnO.backend.user.exception.UserErrorCase; @@ -20,6 +23,8 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -30,7 +35,10 @@ public class StudyRoomSharedProblemCommentService { private final StudyRoomAccessService accessService; private final StudyRoomSharedProblemRepository sharedProblemRepository; private final StudyRoomSharedProblemCommentRepository commentRepository; + private final StudyRoomSharedProblemCommentReactionRepository commentReactionRepository; private final UserRepository userRepository; + private final StudyRoomReactionService reactionService; + private final CustomEmojiValidator customEmojiValidator; @Transactional(readOnly = true) public CursorPageResponse getComments(Long roomId, Long sharedProblemId, @@ -42,8 +50,15 @@ public CursorPageResponse getComments(Long roomId, sharedProblemId, cursor, PageRequest.of(0, safeSize + 1)); boolean hasNext = comments.size() > safeSize; List contentComments = hasNext ? comments.subList(0, safeSize) : comments; + List commentIds = contentComments.stream() + .map(StudyRoomSharedProblemComment::getId) + .toList(); + Map> reactions = commentIds.isEmpty() + ? Map.of() + : commentReactionRepository.findAllByCommentIds(commentIds).stream() + .collect(Collectors.groupingBy(reaction -> reaction.getComment().getId())); List content = contentComments.stream() - .map(comment -> toResponse(comment, userId)) + .map(comment -> toResponse(comment, reactions.getOrDefault(comment.getId(), List.of()), userId)) .toList(); Long nextCursor = hasNext && !contentComments.isEmpty() ? contentComments.get(contentComments.size() - 1).getId() @@ -60,7 +75,7 @@ public SharedProblemCommentResponse createComment(Long roomId, Long sharedProble .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); StudyRoomSharedProblemComment comment = commentRepository.save( StudyRoomSharedProblemComment.create(sharedProblem, user, validateContent(request))); - return toResponse(comment, userId, false); + return toResponse(comment, List.of(), userId, false); } @Transactional @@ -72,7 +87,22 @@ public SharedProblemCommentResponse updateComment(Long roomId, Long sharedProble throw new ApplicationException(StudyRoomErrorCase.SHARED_PROBLEM_COMMENT_FORBIDDEN); } comment.updateContent(validateContent(request)); - return toResponse(comment, userId, true); + return toResponse(comment, commentReactionRepository.findAllByCommentId(commentId), userId, true); + } + + @Transactional + public SharedProblemCommentReactionToggleResponse toggleReaction(Long roomId, Long sharedProblemId, Long commentId, + Long userId, ReactionToggleRequest request) { + accessService.validateMember(roomId, userId); + customEmojiValidator.validate(request.emoji()); + StudyRoomSharedProblemComment comment = getCommentOrThrow(roomId, sharedProblemId, commentId); + User user = userRepository.findById(userId) + .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); + commentReactionRepository.findByCommentIdAndUserIdAndEmoji(commentId, userId, request.emoji()) + .ifPresentOrElse(commentReactionRepository::delete, + () -> commentReactionRepository.save(StudyRoomSharedProblemCommentReaction.create(comment, user, request.emoji()))); + return new SharedProblemCommentReactionToggleResponse(commentId, + reactionService.summarizeSharedProblemCommentReactions(commentReactionRepository.findAllByCommentId(commentId), userId)); } @Transactional @@ -84,6 +114,7 @@ public void deleteComment(Long roomId, Long sharedProblemId, Long commentId, Lon if (!mine && !host) { throw new ApplicationException(StudyRoomErrorCase.SHARED_PROBLEM_COMMENT_FORBIDDEN); } + commentReactionRepository.deleteByCommentId(commentId); commentRepository.delete(comment); } @@ -106,10 +137,19 @@ private String validateContent(SharedProblemCommentRequest request) { } private SharedProblemCommentResponse toResponse(StudyRoomSharedProblemComment comment, Long userId) { - return toResponse(comment, userId, false); + return toResponse(comment, List.of(), userId, false); + } + + private SharedProblemCommentResponse toResponse(StudyRoomSharedProblemComment comment, + List reactions, + Long userId) { + return toResponse(comment, reactions, userId, false); } - private SharedProblemCommentResponse toResponse(StudyRoomSharedProblemComment comment, Long userId, boolean forceEdited) { + private SharedProblemCommentResponse toResponse(StudyRoomSharedProblemComment comment, + List reactions, + Long userId, + boolean forceEdited) { LocalDateTime createdAt = comment.getCreatedAt(); LocalDateTime updatedAt = comment.getUpdatedAt(); Long authorId = comment.getAuthor().getId(); @@ -120,12 +160,13 @@ private SharedProblemCommentResponse toResponse(StudyRoomSharedProblemComment co comment.getContent(), authorId, comment.getAuthor().getName(), - null, + comment.getAuthor().getProfileImageUrl(), createdAt, updatedAt, forceEdited || createdAt != null && updatedAt != null && updatedAt.isAfter(createdAt), mine, - canDelete + canDelete, + reactionService.summarizeSharedProblemCommentReactions(reactions, userId) ); } } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java index 2c3e75c8..7a88da06 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java @@ -33,6 +33,7 @@ public class StudyRoomSharedProblemService { private final StudyRoomSharedProblemRepository sharedProblemRepository; private final StudyRoomSharedProblemReactionRepository reactionRepository; private final StudyRoomSharedProblemCommentRepository commentRepository; + private final StudyRoomSharedProblemCommentReactionRepository commentReactionRepository; private final UserRepository userRepository; private final ProblemService problemService; private final StudyRoomReactionService reactionService; @@ -46,12 +47,23 @@ public CursorPageResponse getSharedProblems(Long roomId, List sharedProblems = sharedProblemRepository.findByRoomIdAndCursor(roomId, cursor, PageRequest.of(0, safeSize + 1)); boolean hasNext = sharedProblems.size() > safeSize; List contentSharedProblems = hasNext ? sharedProblems.subList(0, safeSize) : sharedProblems; - Map> reactions = reactionRepository.findAllBySharedProblemIds( - contentSharedProblems.stream().map(StudyRoomSharedProblem::getId).toList()) - .stream() + List sharedProblemIds = contentSharedProblems.stream() + .map(StudyRoomSharedProblem::getId) + .toList(); + Map> reactions = sharedProblemIds.isEmpty() + ? Map.of() + : reactionRepository.findAllBySharedProblemIds(sharedProblemIds).stream() .collect(Collectors.groupingBy(reaction -> reaction.getSharedProblem().getId())); + Map commentCounts = sharedProblemIds.isEmpty() + ? Map.of() + : commentRepository.countBySharedProblemIds(sharedProblemIds).stream() + .collect(Collectors.toMap(row -> (Long) row[0], row -> (Long) row[1])); List content = contentSharedProblems.stream() - .map(sharedProblem -> toResponse(sharedProblem, reactions.getOrDefault(sharedProblem.getId(), List.of()), userId)) + .map(sharedProblem -> toResponse( + sharedProblem, + reactions.getOrDefault(sharedProblem.getId(), List.of()), + commentCounts.getOrDefault(sharedProblem.getId(), 0L), + userId)) .toList(); Long nextCursor = hasNext && !contentSharedProblems.isEmpty() ? contentSharedProblems.get(contentSharedProblems.size() - 1).getId() @@ -73,7 +85,7 @@ public SharedProblemResponse shareProblem(Long roomId, Long userId, SharedProble eventPublisher.publishEvent(new StudyRoomActivityEvent(userId, StudyRoomFeedEventType.PROBLEM_SHARED, Map.of("reference", problem.getReference() == null ? "" : problem.getReference(), "sharedProblemId", sharedProblem.getId()))); - return toResponse(sharedProblem, List.of(), userId); + return toResponse(sharedProblem, List.of(), 0L, userId); } @Transactional @@ -99,6 +111,7 @@ public void deleteSharedProblem(Long roomId, Long sharedProblemId, Long userId) if (!sharedProblem.getSharedByUser().getId().equals(userId)) { throw new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_FORBIDDEN); } + commentReactionRepository.deleteBySharedProblemId(sharedProblemId); commentRepository.deleteBySharedProblemId(sharedProblemId); reactionRepository.deleteBySharedProblemId(sharedProblemId); sharedProblemRepository.delete(sharedProblem); @@ -106,25 +119,38 @@ public void deleteSharedProblem(Long roomId, Long sharedProblemId, Long userId) private SharedProblemResponse toResponse(StudyRoomSharedProblem sharedProblem, List reactions, + long commentCount, Long userId) { Problem problem = sharedProblem.getProblem(); return new SharedProblemResponse( sharedProblem.getId(), sharedProblem.getSharedByUser().getId(), sharedProblem.getSharedByUser().getName(), + sharedProblem.getSharedByUser().getProfileImageUrl(), problem.getId(), problemImageUrl(problem), - problem.getReference(), + problemImageUrls(problem), + referenceOrFallback(problem.getReference()), sharedProblem.getComment(), + commentCount, sharedProblem.getCreatedAt(), reactionService.summarizeSharedProblemReactions(reactions, userId) ); } - private String problemImageUrl(Problem problem) { + private String referenceOrFallback(String reference) { + return reference == null || reference.isBlank() ? "공유 문제" : reference; + } + + private List problemImageUrls(Problem problem) { return problem.getProblemImageDataList().stream() .filter(image -> image.getProblemImageType() == ProblemImageType.PROBLEM_IMAGE) .map(ProblemImageData::getImageUrl) + .toList(); + } + + private String problemImageUrl(Problem problem) { + return problemImageUrls(problem).stream() .findFirst() .orElse(null); } diff --git a/src/main/java/com/aisip/OnO/backend/user/controller/UserController.java b/src/main/java/com/aisip/OnO/backend/user/controller/UserController.java index 316a80f5..2c80c1e6 100644 --- a/src/main/java/com/aisip/OnO/backend/user/controller/UserController.java +++ b/src/main/java/com/aisip/OnO/backend/user/controller/UserController.java @@ -7,8 +7,10 @@ import com.aisip.OnO.backend.user.dto.UserResponseDto; import com.aisip.OnO.backend.user.service.UserService; import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; @RestController @RequiredArgsConstructor @@ -47,6 +49,18 @@ public CommonResponse updateNotificationSettings( return CommonResponse.success("알림 설정이 변경되었습니다."); } + @PatchMapping(value = "/me/profile-image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public CommonResponse updateProfileImage(@RequestParam("profileImage") MultipartFile profileImage) { + Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + return CommonResponse.success(userService.updateProfileImage(userId, profileImage)); + } + + @DeleteMapping("/me/profile-image") + public CommonResponse deleteProfileImage() { + Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + return CommonResponse.success(userService.deleteProfileImage(userId)); + } + // ✅ 사용자 계정 삭제 @DeleteMapping("") public void deleteUserInfo() { diff --git a/src/main/java/com/aisip/OnO/backend/user/dto/UserResponseDto.java b/src/main/java/com/aisip/OnO/backend/user/dto/UserResponseDto.java index e80e5b4c..acdee406 100644 --- a/src/main/java/com/aisip/OnO/backend/user/dto/UserResponseDto.java +++ b/src/main/java/com/aisip/OnO/backend/user/dto/UserResponseDto.java @@ -12,6 +12,7 @@ public record UserResponseDto ( Long userId, String name, String email, + String profileImageUrl, Long attendanceLevel, Long attendancePoint, Long noteWriteLevel, @@ -35,6 +36,7 @@ public static UserResponseDto from(@NotNull User user) { .userId(user.getId()) .name(user.getName()) .email(user.getEmail()) + .profileImageUrl(user.getProfileImageUrl()) .attendanceLevel(getResponseLevel(missionStatus.getAttendanceLevel())) .attendancePoint(getResponsePoint(missionStatus.getAttendanceLevel(), missionStatus.getAttendancePoint())) .noteWriteLevel(getResponseLevel(missionStatus.getNoteWriteLevel())) diff --git a/src/main/java/com/aisip/OnO/backend/user/entity/User.java b/src/main/java/com/aisip/OnO/backend/user/entity/User.java index eb9090e1..ec4283ff 100644 --- a/src/main/java/com/aisip/OnO/backend/user/entity/User.java +++ b/src/main/java/com/aisip/OnO/backend/user/entity/User.java @@ -43,6 +43,9 @@ public class User extends BaseEntity { private String platform; + @Column(name = "profile_image_url") + private String profileImageUrl; + private LocalDateTime lastActiveAt; private LocalDate lastNotifiedAt; @@ -91,6 +94,10 @@ public void updateNotificationEnabled(boolean enabled) { this.notificationEnabled = enabled; } + public void updateProfileImageUrl(String profileImageUrl) { + this.profileImageUrl = profileImageUrl; + } + public void updateUser(UserRegisterDto userRegisterDto) { if (userRegisterDto.email() != null && !userRegisterDto.email().isBlank()) { this.email = userRegisterDto.email(); diff --git a/src/main/java/com/aisip/OnO/backend/user/service/UserService.java b/src/main/java/com/aisip/OnO/backend/user/service/UserService.java index 311ed93a..e1aaad1d 100644 --- a/src/main/java/com/aisip/OnO/backend/user/service/UserService.java +++ b/src/main/java/com/aisip/OnO/backend/user/service/UserService.java @@ -5,20 +5,28 @@ import com.aisip.OnO.backend.practicenote.service.PracticeNoteService; import com.aisip.OnO.backend.problem.service.ProblemService; import com.aisip.OnO.backend.studyroom.repository.StudyRoomSharedProblemCommentRepository; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomSharedProblemCommentReactionRepository; import com.aisip.OnO.backend.user.dto.UserRegisterDto; import com.aisip.OnO.backend.user.dto.UserResponseDto; import com.aisip.OnO.backend.user.entity.User; import com.aisip.OnO.backend.user.exception.UserErrorCase; import com.aisip.OnO.backend.common.exception.ApplicationException; import com.aisip.OnO.backend.user.repository.UserRepository; +import com.aisip.OnO.backend.util.fileupload.exception.FileUploadErrorCase; +import com.aisip.OnO.backend.util.fileupload.service.FileUploadService; import com.aisip.OnO.backend.util.webhook.DiscordWebhookNotificationService; +import com.aisip.OnO.backend.config.rabbitmq.producer.S3DeleteProducer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.sql.Date; import java.sql.Timestamp; import java.time.LocalDate; @@ -26,6 +34,7 @@ import java.time.LocalTime; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; @@ -34,6 +43,7 @@ @Service @RequiredArgsConstructor public class UserService { + private static final long MAX_PROFILE_IMAGE_SIZE_BYTES = 5 * 1024 * 1024; private final UserRepository userRepository; @@ -45,6 +55,12 @@ public class UserService { private final StudyRoomSharedProblemCommentRepository sharedProblemCommentRepository; + private final StudyRoomSharedProblemCommentReactionRepository sharedProblemCommentReactionRepository; + + private final FileUploadService fileUploadService; + + private final S3DeleteProducer s3DeleteProducer; + private final DiscordWebhookNotificationService discordWebhookNotificationService; private User findUserEntity(Long userId){ @@ -143,9 +159,33 @@ public void updateUser(Long userId, UserRegisterDto userRegisterDto) { userRepository.save(user); } + @Transactional + public UserResponseDto updateProfileImage(Long userId, MultipartFile profileImage) { + validateProfileImage(profileImage); + User user = findUserEntity(userId); + String previousProfileImageUrl = user.getProfileImageUrl(); + String profileImageUrl = fileUploadService.uploadFileToS3(profileImage); + deleteImageOnRollback(profileImageUrl, userId); + user.updateProfileImageUrl(profileImageUrl); + deleteImageAsync(previousProfileImageUrl, userId); + return UserResponseDto.from(user); + } + + @Transactional + public UserResponseDto deleteProfileImage(Long userId) { + User user = findUserEntity(userId); + String previousProfileImageUrl = user.getProfileImageUrl(); + user.updateProfileImageUrl(null); + deleteImageAsync(previousProfileImageUrl, userId); + return UserResponseDto.from(user); + } + @Transactional public void deleteUserById(Long userId) { User user = findUserEntity(userId); + deleteImageAsync(user.getProfileImageUrl(), userId); + sharedProblemCommentReactionRepository.deleteByCommentAuthorId(userId); + sharedProblemCommentReactionRepository.deleteByUserId(userId); sharedProblemCommentRepository.deleteByAuthorId(userId); practiceNoteService.deleteAllPracticesByUser(userId); problemService.deleteAllUserProblems(userId); @@ -164,6 +204,111 @@ private String makeDeletedIdentifier(Long userId) { return "deleted:" + userId + ":" + UUID.randomUUID(); } + private void validateProfileImage(MultipartFile profileImage) { + if (profileImage == null || profileImage.isEmpty()) { + throw new ApplicationException(FileUploadErrorCase.INVALID_IMAGE_FILE); + } + if (profileImage.getSize() > MAX_PROFILE_IMAGE_SIZE_BYTES) { + throw new ApplicationException(FileUploadErrorCase.FILE_SIZE_EXCEEDED); + } + + String extension = getLowercaseExtension(profileImage.getOriginalFilename()); + String contentType = profileImage.getContentType(); + if (!isAllowedImageType(contentType, extension) || !hasAllowedImageSignature(profileImage, contentType)) { + throw new ApplicationException(FileUploadErrorCase.INVALID_IMAGE_FILE); + } + } + + private String getLowercaseExtension(String originalFilename) { + if (originalFilename == null) { + throw new ApplicationException(FileUploadErrorCase.INVALID_IMAGE_FILE); + } + int dotIndex = originalFilename.lastIndexOf('.'); + if (dotIndex < 0 || dotIndex == originalFilename.length() - 1) { + throw new ApplicationException(FileUploadErrorCase.INVALID_IMAGE_FILE); + } + return originalFilename.substring(dotIndex + 1).toLowerCase(Locale.ROOT); + } + + private boolean isAllowedImageType(String contentType, String extension) { + if (contentType == null) { + return false; + } + return switch (contentType) { + case "image/jpeg" -> extension.equals("jpg") || extension.equals("jpeg"); + case "image/png" -> extension.equals("png"); + case "image/webp" -> extension.equals("webp"); + default -> false; + }; + } + + private boolean hasAllowedImageSignature(MultipartFile profileImage, String contentType) { + byte[] header = new byte[12]; + int read; + try { + read = profileImage.getInputStream().read(header); + } catch (IOException e) { + throw new ApplicationException(FileUploadErrorCase.FILE_UPLOAD_FAILED); + } + + return switch (contentType) { + case "image/jpeg" -> read >= 3 + && (header[0] & 0xFF) == 0xFF + && (header[1] & 0xFF) == 0xD8 + && (header[2] & 0xFF) == 0xFF; + case "image/png" -> read >= 8 + && (header[0] & 0xFF) == 0x89 + && header[1] == 0x50 + && header[2] == 0x4E + && header[3] == 0x47 + && header[4] == 0x0D + && header[5] == 0x0A + && header[6] == 0x1A + && header[7] == 0x0A; + case "image/webp" -> read >= 12 + && header[0] == 0x52 + && header[1] == 0x49 + && header[2] == 0x46 + && header[3] == 0x46 + && header[8] == 0x57 + && header[9] == 0x45 + && header[10] == 0x42 + && header[11] == 0x50; + default -> false; + }; + } + + private void deleteImageAsync(String imageUrl, Long userId) { + if (imageUrl == null || imageUrl.isBlank()) { + return; + } + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + s3DeleteProducer.sendDeleteMessage(imageUrl, userId); + return; + } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + s3DeleteProducer.sendDeleteMessage(imageUrl, userId); + } + }); + } + + private void deleteImageOnRollback(String imageUrl, Long userId) { + if (imageUrl == null || imageUrl.isBlank() + || !TransactionSynchronizationManager.isSynchronizationActive()) { + return; + } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCompletion(int status) { + if (status == STATUS_ROLLED_BACK) { + s3DeleteProducer.sendDeleteMessage(imageUrl, userId); + } + } + }); + } + private String makeGuestName() { return "Guest" + UUID.randomUUID().toString().substring(0, 8); } diff --git a/src/main/resources/db/migration/V11__add_comment_reactions_and_user_profile_image.sql b/src/main/resources/db/migration/V11__add_comment_reactions_and_user_profile_image.sql new file mode 100644 index 00000000..365de366 --- /dev/null +++ b/src/main/resources/db/migration/V11__add_comment_reactions_and_user_profile_image.sql @@ -0,0 +1,18 @@ +ALTER TABLE user + ADD COLUMN profile_image_url VARCHAR(255); + +CREATE TABLE study_room_shared_problem_comment_reaction ( + id BIGINT NOT NULL AUTO_INCREMENT, + comment_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + emoji VARCHAR(80) NOT NULL, + created_at DATETIME(6), + updated_at DATETIME(6), + deleted_at DATETIME(6), + PRIMARY KEY (id), + CONSTRAINT uk_shared_problem_comment_reaction UNIQUE (comment_id, user_id, emoji), + CONSTRAINT fk_shared_problem_comment_reaction_comment FOREIGN KEY (comment_id) REFERENCES study_room_shared_problem_comment (id) ON DELETE CASCADE, + CONSTRAINT fk_shared_problem_comment_reaction_user FOREIGN KEY (user_id) REFERENCES user (id) +); +CREATE INDEX idx_shared_problem_comment_reaction_comment ON study_room_shared_problem_comment_reaction (comment_id); +CREATE INDEX idx_shared_problem_comment_reaction_user ON study_room_shared_problem_comment_reaction (user_id); diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemApiTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemApiTest.java index f83c5dd5..34e1dbbe 100644 --- a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemApiTest.java +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemApiTest.java @@ -1,7 +1,10 @@ package com.aisip.OnO.backend.studyroom.integration; import com.aisip.OnO.backend.problem.dto.ProblemRegisterDto; +import com.aisip.OnO.backend.problem.dto.ProblemImageDataRegisterDto; import com.aisip.OnO.backend.problem.entity.Problem; +import com.aisip.OnO.backend.problem.entity.ProblemImageData; +import com.aisip.OnO.backend.problem.entity.ProblemImageType; import com.aisip.OnO.backend.problem.repository.ProblemRepository; import com.aisip.OnO.backend.practicenote.dto.PracticeNoteRegisterDto; import com.aisip.OnO.backend.problemsolve.dto.ProblemSolveRegisterDto; @@ -41,6 +44,8 @@ @ActiveProfiles("test") class StudyRoomSharedProblemApiTest { + private static final String EMOJI = "fired_up_sparkle_eyes"; + @Autowired private MockMvc mockMvc; @@ -126,6 +131,13 @@ void sharerCanDeleteOwnSharedProblem() throws Exception { // host가 공유 후 본인 삭제 Long sharedProblemId = shareProblem(roomId, problem.getId(), "내 공유 문제"); + Long commentId = createComment(roomId, sharedProblemId, "삭제 전 댓글"); + mockMvc.perform(post("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments/{commentId}/reactions", + roomId, sharedProblemId, commentId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ReactionToggleRequest(EMOJI)))) + .andExpect(status().isOk()); mockMvc.perform(delete("/api/study-room/{roomId}/shared-problems/{sharedProblemId}", roomId, sharedProblemId) .with(auth())) @@ -179,6 +191,33 @@ void sharedProblemPaginationWithCursor() throws Exception { assertThat(secondContent).hasSize(2); } + @Test + void sharedProblemListIncludesDetailFieldsForBoardSheet() throws Exception { + User host = saveUser("방장", "shared-detail-host"); + authenticate(host.getId()); + Long roomId = createRoom("공유 문제 상세 응답방"); + Problem problem = createProblem(host.getId(), "", "https://cdn.example.com/problem.png"); + Long sharedProblemId = shareProblem(roomId, problem.getId(), "풀이 의견을 남겨주세요"); + + createComment(roomId, sharedProblemId, "첫 번째 의견"); + createComment(roomId, sharedProblemId, "두 번째 의견"); + + mockMvc.perform(get("/api/study-room/{roomId}/shared-problems", roomId) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.content[0].sharedProblemId").value(sharedProblemId)) + .andExpect(jsonPath("$.data.content[0].sharedByUserId").value(host.getId())) + .andExpect(jsonPath("$.data.content[0].sharedByName").value("방장")) + .andExpect(jsonPath("$.data.content[0].sharedByProfileImageUrl").isEmpty()) + .andExpect(jsonPath("$.data.content[0].problemId").value(problem.getId())) + .andExpect(jsonPath("$.data.content[0].problemImageUrl").value("https://cdn.example.com/problem.png")) + .andExpect(jsonPath("$.data.content[0].reference").value("공유 문제")) + .andExpect(jsonPath("$.data.content[0].comment").value("풀이 의견을 남겨주세요")) + .andExpect(jsonPath("$.data.content[0].commentCount").value(2)) + .andExpect(jsonPath("$.data.content[0].sharedAt").exists()) + .andExpect(jsonPath("$.data.content[0].reactions").isEmpty()); + } + @Test void sharedProblemDoesNotAllowOtherMemberToUseOriginalProblemAsOwnStudyData() throws Exception { User host = saveUser("방장", "shared-readonly-host"); @@ -274,10 +313,32 @@ private void join(String code) throws Exception { } private Problem createProblem(Long userId) { - return problemRepository.save(Problem.from( - new ProblemRegisterDto(null, "memo", "ref", null, LocalDateTime.now()), + return createProblem(userId, "ref", null); + } + + private Problem createProblem(Long userId, String reference, String problemImageUrl) { + Problem problem = Problem.from( + new ProblemRegisterDto(null, "memo", reference, null, LocalDateTime.now()), userId - )); + ); + if (problemImageUrl != null) { + ProblemImageData imageData = ProblemImageData.from( + new ProblemImageDataRegisterDto(null, problemImageUrl, ProblemImageType.PROBLEM_IMAGE)); + imageData.updateProblem(problem); + } + return problemRepository.save(problem); + } + + private Long createComment(Long roomId, Long sharedProblemId, String content) throws Exception { + MvcResult result = mockMvc.perform(post("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments", + roomId, sharedProblemId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new SharedProblemCommentRequest(content)))) + .andExpect(status().isCreated()) + .andReturn(); + Number value = JsonPath.read(result.getResponse().getContentAsString(), "$.data.commentId"); + return value.longValue(); } private Long shareProblem(Long roomId, Long problemId, String comment) throws Exception { diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemCommentApiTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemCommentApiTest.java index a6c354d3..7282be6c 100644 --- a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemCommentApiTest.java +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomSharedProblemCommentApiTest.java @@ -37,6 +37,8 @@ @ActiveProfiles("test") class StudyRoomSharedProblemCommentApiTest { + private static final String EMOJI = "fired_up_sparkle_eyes"; + @Autowired private MockMvc mockMvc; @@ -81,7 +83,8 @@ void memberCanCreateUpdateAndListSharedProblemComments() throws Exception { .andExpect(jsonPath("$.data.authorName").value("멤버")) .andExpect(jsonPath("$.data.isEdited").value(true)) .andExpect(jsonPath("$.data.isMine").value(true)) - .andExpect(jsonPath("$.data.canDelete").value(true)); + .andExpect(jsonPath("$.data.canDelete").value(true)) + .andExpect(jsonPath("$.data.reactions").isEmpty()); MvcResult result = mockMvc.perform(get("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments", roomId, sharedProblemId) @@ -92,6 +95,7 @@ void memberCanCreateUpdateAndListSharedProblemComments() throws Exception { .andExpect(jsonPath("$.data.content[0].content").value("수정된 풀이 의견")) .andExpect(jsonPath("$.data.content[0].isMine").value(true)) .andExpect(jsonPath("$.data.content[0].canDelete").value(true)) + .andExpect(jsonPath("$.data.content[0].reactions").isEmpty()) .andReturn(); List comments = JsonPath.read(result.getResponse().getContentAsString(), "$.data.content"); @@ -129,6 +133,14 @@ void onlyAuthorCanUpdateAndHostCanDeleteOtherMemberComment() throws Exception { .andExpect(status().isForbidden()) .andExpect(jsonPath("$.errorCode").value(10019)); + mockMvc.perform(post("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments/{commentId}/reactions", + roomId, sharedProblemId, commentId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ReactionToggleRequest(EMOJI)))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.reactions[0].count").value(1)); + authenticate(host.getId()); mockMvc.perform(get("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments", roomId, sharedProblemId) @@ -181,6 +193,47 @@ void commentPaginationUsesCursor() throws Exception { assertThat(comments).hasSize(2); } + @Test + void memberCanToggleSharedProblemCommentReaction() throws Exception { + User host = saveUser("방장", "comment-reaction-host"); + User member = saveUser("멤버", "comment-reaction-member"); + authenticate(host.getId()); + Long roomId = createRoom("댓글 반응방"); + String inviteCode = issueInviteCode(roomId); + Long sharedProblemId = shareProblem(roomId, createProblem(host.getId()).getId(), "공유 문제"); + Long commentId = createComment(roomId, sharedProblemId, "방장 댓글"); + + authenticate(member.getId()); + join(inviteCode); + + mockMvc.perform(post("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments/{commentId}/reactions", + roomId, sharedProblemId, commentId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ReactionToggleRequest(EMOJI)))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.commentId").value(commentId)) + .andExpect(jsonPath("$.data.reactions[0].emoji").value(EMOJI)) + .andExpect(jsonPath("$.data.reactions[0].count").value(1)) + .andExpect(jsonPath("$.data.reactions[0].reactedByMe").value(true)); + + mockMvc.perform(get("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments", + roomId, sharedProblemId) + .with(auth())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.content[0].reactions[0].emoji").value(EMOJI)) + .andExpect(jsonPath("$.data.content[0].reactions[0].count").value(1)) + .andExpect(jsonPath("$.data.content[0].reactions[0].reactedByMe").value(true)); + + mockMvc.perform(post("/api/study-rooms/{roomId}/shared-problems/{sharedProblemId}/comments/{commentId}/reactions", + roomId, sharedProblemId, commentId) + .with(auth()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ReactionToggleRequest(EMOJI)))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.reactions").isEmpty()); + } + @Test void blankOrTooLongCommentIsRejected() throws Exception { User host = saveUser("방장", "comment-invalid-host"); diff --git a/src/test/java/com/aisip/OnO/backend/user/controller/UserControllerTest.java b/src/test/java/com/aisip/OnO/backend/user/controller/UserControllerTest.java index c062f266..4c540f31 100644 --- a/src/test/java/com/aisip/OnO/backend/user/controller/UserControllerTest.java +++ b/src/test/java/com/aisip/OnO/backend/user/controller/UserControllerTest.java @@ -50,6 +50,7 @@ void setUp() { 1L, // userId "testUser", // name "test@example.com", // email + null, 1L, // level 0L, // point 1L, diff --git a/src/test/java/com/aisip/OnO/backend/user/service/UserServiceTest.java b/src/test/java/com/aisip/OnO/backend/user/service/UserServiceTest.java index b3e0f2b8..96dae6a6 100644 --- a/src/test/java/com/aisip/OnO/backend/user/service/UserServiceTest.java +++ b/src/test/java/com/aisip/OnO/backend/user/service/UserServiceTest.java @@ -4,12 +4,15 @@ import com.aisip.OnO.backend.folder.service.FolderService; import com.aisip.OnO.backend.practicenote.service.PracticeNoteService; import com.aisip.OnO.backend.problem.service.ProblemService; +import com.aisip.OnO.backend.config.rabbitmq.producer.S3DeleteProducer; import com.aisip.OnO.backend.studyroom.repository.StudyRoomSharedProblemCommentRepository; +import com.aisip.OnO.backend.studyroom.repository.StudyRoomSharedProblemCommentReactionRepository; import com.aisip.OnO.backend.user.dto.UserRegisterDto; import com.aisip.OnO.backend.user.dto.UserResponseDto; import com.aisip.OnO.backend.user.entity.User; import com.aisip.OnO.backend.user.exception.UserErrorCase; import com.aisip.OnO.backend.user.repository.UserRepository; +import com.aisip.OnO.backend.util.fileupload.service.FileUploadService; import com.aisip.OnO.backend.util.webhook.DiscordWebhookNotificationService; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -19,6 +22,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; import java.util.List; import java.util.Optional; @@ -44,6 +48,12 @@ class UserServiceTest { @Mock private StudyRoomSharedProblemCommentRepository sharedProblemCommentRepository; @Mock + private StudyRoomSharedProblemCommentReactionRepository sharedProblemCommentReactionRepository; + @Mock + private FileUploadService fileUploadService; + @Mock + private S3DeleteProducer s3DeleteProducer; + @Mock private DiscordWebhookNotificationService discordWebhookNotificationService; @InjectMocks @@ -250,6 +260,8 @@ void deleteUserById() { // Then assertThat(user.getIdentifier()).startsWith("deleted:" + userId + ":"); + verify(sharedProblemCommentReactionRepository, times(1)).deleteByCommentAuthorId(userId); + verify(sharedProblemCommentReactionRepository, times(1)).deleteByUserId(userId); verify(sharedProblemCommentRepository, times(1)).deleteByAuthorId(userId); verify(practiceNoteService, times(1)).deleteAllPracticesByUser(userId); verify(problemService, times(1)).deleteAllUserProblems(userId); @@ -258,4 +270,51 @@ void deleteUserById() { verify(userRepository, times(1)).deleteById(userId); verify(userRepository, times(2)).flush(); } + + @Test + @DisplayName("프로필 이미지 업로드") + void updateProfileImage() { + // Given + Long userId = 1L; + User user = User.from(userRegisterDto); + MockMultipartFile profileImage = pngFile(); + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(fileUploadService.uploadFileToS3(any())).thenReturn("https://cdn.example.com/profile.png"); + + // When + UserResponseDto response = userService.updateProfileImage(userId, profileImage); + + // Then + assertThat(response.profileImageUrl()).isEqualTo("https://cdn.example.com/profile.png"); + assertThat(user.getProfileImageUrl()).isEqualTo("https://cdn.example.com/profile.png"); + verify(fileUploadService, times(1)).uploadFileToS3(profileImage); + verify(s3DeleteProducer, never()).sendDeleteMessage(any(), any()); + } + + @Test + @DisplayName("프로필 이미지 삭제") + void deleteProfileImage() { + // Given + Long userId = 1L; + User user = User.from(userRegisterDto); + user.updateProfileImageUrl("https://cdn.example.com/old-profile.png"); + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + + // When + UserResponseDto response = userService.deleteProfileImage(userId); + + // Then + assertThat(response.profileImageUrl()).isNull(); + assertThat(user.getProfileImageUrl()).isNull(); + verify(s3DeleteProducer, times(1)).sendDeleteMessage("https://cdn.example.com/old-profile.png", userId); + } + + private MockMultipartFile pngFile() { + byte[] content = new byte[] { + (byte) 0x89, 0x50, 0x4E, 0x47, + 0x0D, 0x0A, 0x1A, 0x0A, + 0x00, 0x00, 0x00, 0x00 + }; + return new MockMultipartFile("profileImage", "profile.png", "image/png", content); + } } From 96cb61f77d5a666c803fb9af9bdc09285d176b41 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Mon, 29 Jun 2026 11:22:58 +0900 Subject: [PATCH 39/67] =?UTF-8?q?[Fix]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20Rab?= =?UTF-8?q?bit=20listener=20=EC=9E=90=EB=8F=99=20=EC=8B=9C=EC=9E=91=20?= =?UTF-8?q?=EB=B0=A9=EC=A7=80=20-=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=8C=8C=EC=9D=BC=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aisip/OnO/backend/config/rabbitmq/RabbitMQConfig.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/config/rabbitmq/RabbitMQConfig.java b/src/main/java/com/aisip/OnO/backend/config/rabbitmq/RabbitMQConfig.java index 68069418..5bd61a59 100644 --- a/src/main/java/com/aisip/OnO/backend/config/rabbitmq/RabbitMQConfig.java +++ b/src/main/java/com/aisip/OnO/backend/config/rabbitmq/RabbitMQConfig.java @@ -6,6 +6,7 @@ import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -61,10 +62,12 @@ public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { */ @Bean public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory( - ConnectionFactory connectionFactory) { + ConnectionFactory connectionFactory, + @Value("${spring.rabbitmq.listener.simple.auto-startup:true}") boolean autoStartup) { SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); factory.setMessageConverter(messageConverter()); + factory.setAutoStartup(autoStartup); factory.setConcurrentConsumers(3); // 동시 처리 스레드 수 factory.setMaxConcurrentConsumers(10); // 최대 동시 처리 스레드 수 factory.setPrefetchCount(1); // 한 번에 가져올 메시지 수 @@ -199,4 +202,4 @@ public Binding fcmNotificationBinding() { .to(notificationExchange()) .with(FCM_NOTIFICATION_ROUTING_KEY); } -} \ No newline at end of file +} From 5bd131aaf52fe65565bc4098c7f57c3f8262beb0 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Mon, 29 Jun 2026 14:57:53 +0900 Subject: [PATCH 40/67] =?UTF-8?q?[Feat]=20=EA=B3=B5=EC=9C=A0=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=A4=91=EB=B3=B5=20=EA=B3=B5=EC=9C=A0=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80=20-=20409=20ALREADY=5FSHARED=5FPROBLEM=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EC=B6=94=EA=B0=80=20-=20roomId=20+=20problemId=20?= =?UTF-8?q?=EC=A1=B0=ED=95=A9=20=EC=A4=91=EB=B3=B5=20=EC=8B=9C=20HTTP=2040?= =?UTF-8?q?9=20=EB=B0=98=ED=99=98=20-=20StudyRoomErrorCase=EC=97=90=20ALRE?= =?UTF-8?q?ADY=5FSHARED=5FPROBLEM(409,=2010020)=20=EC=B6=94=EA=B0=80=20-?= =?UTF-8?q?=20existsByRoomIdAndProblemId=20=ED=8C=8C=EC=83=9D=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=EB=A1=9C=20=EC=A0=80=EC=9E=A5=20=EC=A0=84=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EC=B2=B4=ED=81=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../OnO/backend/studyroom/exception/StudyRoomErrorCase.java | 3 ++- .../studyroom/repository/StudyRoomSharedProblemRepository.java | 2 ++ .../studyroom/service/StudyRoomSharedProblemService.java | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/exception/StudyRoomErrorCase.java b/src/main/java/com/aisip/OnO/backend/studyroom/exception/StudyRoomErrorCase.java index ffcbec6e..3526e538 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/exception/StudyRoomErrorCase.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/exception/StudyRoomErrorCase.java @@ -26,7 +26,8 @@ public enum StudyRoomErrorCase implements ErrorCase { INVALID_STUDY_ROOM_REQUEST(400, 10016, "스터디룸 요청 값이 올바르지 않습니다."), SHARED_PROBLEM_COMMENT_NOT_FOUND(404, 10017, "공유 문제 댓글을 찾을 수 없습니다."), INVALID_SHARED_PROBLEM_COMMENT(400, 10018, "공유 문제 댓글 내용이 올바르지 않습니다."), - SHARED_PROBLEM_COMMENT_FORBIDDEN(403, 10019, "공유 문제 댓글에 대한 권한이 없습니다."); + SHARED_PROBLEM_COMMENT_FORBIDDEN(403, 10019, "공유 문제 댓글에 대한 권한이 없습니다."), + ALREADY_SHARED_PROBLEM(409, 10020, "이미 공유된 문제입니다."); private final Integer httpStatusCode; private final Integer errorCode; diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemRepository.java index 372cd69a..06587ad4 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemRepository.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomSharedProblemRepository.java @@ -23,4 +23,6 @@ List findByRoomIdAndCursor(@Param("roomId") Long roomId, Pageable pageable); Optional findByIdAndRoomId(Long id, Long roomId); + + boolean existsByRoomIdAndProblemId(Long roomId, Long problemId); } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java index 7a88da06..8b62e288 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java @@ -77,6 +77,9 @@ public SharedProblemResponse shareProblem(Long roomId, Long userId, SharedProble if (request.problemId() == null || request.comment() != null && request.comment().length() > 100) { throw new ApplicationException(StudyRoomErrorCase.INVALID_STUDY_ROOM_REQUEST); } + if (sharedProblemRepository.existsByRoomIdAndProblemId(roomId, request.problemId())) { + throw new ApplicationException(StudyRoomErrorCase.ALREADY_SHARED_PROBLEM); + } Problem problem = problemService.findProblemEntityWithImageData(request.problemId(), userId); User user = userRepository.findById(userId) .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); From c8f91af14750412d6ff7a092b3bec6ec3ea484a6 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Mon, 29 Jun 2026 15:29:20 +0900 Subject: [PATCH 41/67] =?UTF-8?q?[Feat]=20=EC=B1=8C=EB=A6=B0=EC=A7=80=20?= =?UTF-8?q?=EC=9E=84=EC=9D=98=20=EC=A3=BC=EA=B8=B0(periodDays)=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=20-=20=EC=8A=A4=ED=82=A4=EB=A7=88=C2=B7DTO=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20study=5Froom=5Fchallenge=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=EC=97=90=20period=5Fdays=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=BC=20=EC=B6=94=EA=B0=80=20(V12)=20-=20ChallengeCreateReq?= =?UTF-8?q?uest=20/=20ChallengeResponse=EC=97=90=20periodDays=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20-=20StudyRoomChallenge=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20periodDays=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=20=EB=B0=8F=20create()=20=EC=8B=9C=EA=B7=B8=EB=8B=88=EC=B2=98?= =?UTF-8?q?=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java | 6 ++++-- .../OnO/backend/studyroom/entity/StudyRoomChallenge.java | 7 ++++++- .../db/migration/V12__add_challenge_period_days.sql | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 src/main/resources/db/migration/V12__add_challenge_period_days.sql diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java index 87dac932..fbc45a4a 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java @@ -26,7 +26,8 @@ public record ReactionToggleRequest(String emoji) { } public record ChallengeCreateRequest(String title, String type, String metric, String period, - Integer targetValue, LocalDateTime startAt, LocalDateTime endAt) { + Integer periodDays, Integer targetValue, + LocalDateTime startAt, LocalDateTime endAt) { } public record SharedProblemCreateRequest(Long problemId, String comment) { @@ -75,7 +76,8 @@ public record FeedReactionToggleResponse(Long feedId, List rea } public record ChallengeResponse(Long challengeId, String title, String type, String metric, String period, - Integer targetValue, LocalDateTime startAt, LocalDateTime endAt, String status, + Integer periodDays, Integer targetValue, + LocalDateTime startAt, LocalDateTime endAt, String status, List memberProgress, Integer groupCurrent) { } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallenge.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallenge.java index ead47f93..220d245c 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallenge.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallenge.java @@ -39,6 +39,9 @@ public class StudyRoomChallenge extends BaseEntity { @Column(name = "period", length = 20) private StudyRoomChallengePeriod period; + @Column(name = "period_days") + private Integer periodDays; + @Column(name = "target_value", nullable = false) private Integer targetValue; @@ -54,13 +57,15 @@ public class StudyRoomChallenge extends BaseEntity { public static StudyRoomChallenge create(StudyRoom room, String title, StudyRoomChallengeType type, StudyRoomChallengeMetric metric, StudyRoomChallengePeriod period, - Integer targetValue, LocalDateTime startAt, LocalDateTime endAt) { + Integer periodDays, Integer targetValue, + LocalDateTime startAt, LocalDateTime endAt) { return StudyRoomChallenge.builder() .room(room) .title(title) .type(type) .metric(metric) .period(period) + .periodDays(periodDays) .targetValue(targetValue) .startAt(startAt) .endAt(endAt) diff --git a/src/main/resources/db/migration/V12__add_challenge_period_days.sql b/src/main/resources/db/migration/V12__add_challenge_period_days.sql new file mode 100644 index 00000000..66978924 --- /dev/null +++ b/src/main/resources/db/migration/V12__add_challenge_period_days.sql @@ -0,0 +1 @@ +ALTER TABLE study_room_challenge ADD COLUMN period_days INT NULL; From 8c0413763d9cfacc2c8a3ae82a396bcb397bc7fc Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Mon, 29 Jun 2026 15:29:27 +0900 Subject: [PATCH 42/67] =?UTF-8?q?[Feat]=20=EC=B1=8C=EB=A6=B0=EC=A7=80=20?= =?UTF-8?q?=EC=9E=84=EC=9D=98=20=EC=A3=BC=EA=B8=B0=20=EC=A7=91=EA=B3=84=20?= =?UTF-8?q?=EB=B0=8F=20=ED=9E=88=EC=8A=A4=ED=86=A0=EB=A6=AC=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B5=AC=ED=98=84=20-=20periodDays=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EC=8A=AC=EB=9D=BC=EC=9D=B4=EB=94=A9=20=EC=9C=88?= =?UTF-8?q?=EB=8F=84=EC=9A=B0=20=EC=A7=91=EA=B3=84=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20period=20+=20periodDays=20=EB=8F=99?= =?UTF-8?q?=EC=8B=9C=20=EC=9E=85=EB=A0=A5=20/=20periodDays=20<=201=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20-=20periodDa?= =?UTF-8?q?ys=20=EC=9E=88=EB=8A=94=20=EC=B1=8C=EB=A6=B0=EC=A7=80=EB=8A=94?= =?UTF-8?q?=20=EC=9E=90=EB=8F=99=20COMPLETED=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8=20-=20getChallenges:=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EC=B1=8C=EB=A6=B0=EC=A7=80=20=EB=B0=98=ED=99=98=20(in=5Fprogre?= =?UTF-8?q?ss=20=EC=9A=B0=EC=84=A0,=20=EB=82=98=EB=A8=B8=EC=A7=80=20endAt?= =?UTF-8?q?=20=EC=97=AD=EC=88=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../service/StudyRoomChallengeService.java | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java index 5980e2de..5dac61cc 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java @@ -13,6 +13,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -28,12 +29,14 @@ public class StudyRoomChallengeService { private final StudyRoomMemberRepository memberRepository; private final StudyRoomStatsService statsService; - @Transactional(readOnly = true) + @Transactional public List getChallenges(Long roomId, Long userId) { accessService.validateMember(roomId, userId); List members = memberRepository.findAllWithUserByRoomId(roomId); - return challengeRepository.findActiveByRoomId(roomId, StudyRoomChallengeStatus.IN_PROGRESS, LocalDateTime.now()).stream() - .map(challenge -> toResponse(challenge, members, false)) + return challengeRepository.findAllByRoomIdOrderByEndAtAsc(roomId).stream() + .map(challenge -> toResponse(challenge, members, true)) + .sorted(Comparator.comparing((ChallengeResponse r) -> "in_progress".equals(r.status()) ? 0 : 1) + .thenComparing(Comparator.comparing(ChallengeResponse::endAt).reversed())) .toList(); } @@ -56,6 +59,7 @@ public ChallengeResponse createChallenge(Long roomId, Long userId, ChallengeCrea parseEnum(request.type(), StudyRoomChallengeType.class), parseEnum(request.metric(), StudyRoomChallengeMetric.class), period, + period == null ? request.periodDays() : null, request.targetValue(), startAt, request.endAt() @@ -112,6 +116,7 @@ private ChallengeResponse toResponse(StudyRoomChallenge challenge, List= challenge.getTargetValue() : !memberProgress.isEmpty() && memberProgress.stream().allMatch(ChallengeMemberProgressResponse::cleared); @@ -160,25 +165,29 @@ private AggregationRange resolveAggregationRange(StudyRoomChallenge challenge, L LocalDateTime startAt = challenge.getStartAt(); LocalDateTime endAt = challenge.getEndAt(); StudyRoomChallengePeriod period = challenge.getPeriod(); - if (period == null) { + Integer periodDays = challenge.getPeriodDays(); + if (period == null && periodDays == null) { return new AggregationRange(startAt, endAt); } LocalDateTime anchor = clamp(now, startAt, endAt); LocalDateTime windowStart = startAt; - LocalDateTime windowEnd = addPeriod(windowStart, period); + LocalDateTime windowEnd = addPeriod(windowStart, period, periodDays); while (!windowEnd.isAfter(anchor) && windowEnd.isBefore(endAt)) { windowStart = windowEnd; - windowEnd = addPeriod(windowStart, period); + windowEnd = addPeriod(windowStart, period, periodDays); } return new AggregationRange(windowStart, windowEnd.isAfter(endAt) ? endAt : windowEnd); } - private LocalDateTime addPeriod(LocalDateTime base, StudyRoomChallengePeriod period) { - return switch (period) { - case DAILY -> base.plusDays(1); - case WEEKLY -> base.plusDays(7); - case MONTHLY -> base.plusMonths(1); - }; + private LocalDateTime addPeriod(LocalDateTime base, StudyRoomChallengePeriod period, Integer periodDays) { + if (period != null) { + return switch (period) { + case DAILY -> base.plusDays(1); + case WEEKLY -> base.plusDays(7); + case MONTHLY -> base.plusMonths(1); + }; + } + return base.plusDays(periodDays); } private LocalDateTime clamp(LocalDateTime value, LocalDateTime lower, LocalDateTime upper) { @@ -197,7 +206,9 @@ private void validateCreateRequest(ChallengeCreateRequest request) { if (request.title() == null || request.title().isBlank() || request.title().length() > 40 || request.targetValue() == null || request.targetValue() < 1 || request.endAt() == null || !request.endAt().isAfter(LocalDateTime.now()) - || request.startAt() != null && !request.startAt().isBefore(request.endAt())) { + || request.startAt() != null && !request.startAt().isBefore(request.endAt()) + || request.period() != null && request.periodDays() != null + || request.periodDays() != null && request.periodDays() < 1) { throw new ApplicationException(StudyRoomErrorCase.INVALID_STUDY_ROOM_REQUEST); } } From 42ffe85a7bd69bcebecc36d59a294ebf14f616fe Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Mon, 29 Jun 2026 15:39:10 +0900 Subject: [PATCH 43/67] =?UTF-8?q?[Feat]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20FCM=20=EC=95=8C=EB=A6=BC=20=EC=B6=94=EA=B0=80=20-?= =?UTF-8?q?=20=EC=B1=8C=EB=A6=B0=EC=A7=80=C2=B7=EA=B3=B5=EC=9C=A0=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=206=EC=A2=85=20-=20=EC=B1=8C=EB=A6=B0?= =?UTF-8?q?=EC=A7=80=20=EA=B8=B0=EA=B0=84=2050%=20=EA=B2=BD=EA=B3=BC=20/?= =?UTF-8?q?=20=EB=A7=88=EA=B0=90=20D-1:=20Quartz=20=EB=8F=99=EC=A0=81=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=80=EC=A4=84=EB=A1=9C=20=EB=A3=B8=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EB=B0=9C=EC=86=A1=20-=20=EC=B1=8C=EB=A6=B0?= =?UTF-8?q?=EC=A7=80=20=EC=82=AD=EC=A0=9C=20=EC=8B=9C=20=EC=98=88=EC=95=BD?= =?UTF-8?q?=EB=90=9C=20=EC=95=8C=EB=A6=BC=20=EC=9E=90=EB=8F=99=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=20-=20=EC=B1=8C=EB=A6=B0=EC=A7=80=20=EB=8B=AC?= =?UTF-8?q?=EC=84=B1(COMPLETED)=20=EC=A0=84=ED=99=98=20=EC=8B=9C=20?= =?UTF-8?q?=EB=A3=B8=20=EC=A0=84=EC=B2=B4=20=EC=A6=89=EC=8B=9C=20=EB=B0=9C?= =?UTF-8?q?=EC=86=A1=20-=20=EA=B3=B5=EC=9C=A0=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=8B=9C=20=EB=A3=B8=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=20=EB=B0=9C=EC=86=A1=20(=EB=93=B1=EB=A1=9D=EC=9E=90=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8)=20-=20=EA=B3=B5=EC=9C=A0=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=9D=B4=EB=AA=A8=EC=A7=80=20=EB=B0=98=EC=9D=91=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EC=8B=9C=20=EB=AC=B8=EC=A0=9C=20=EA=B3=B5?= =?UTF-8?q?=EC=9C=A0=EC=9E=90=EC=97=90=EA=B2=8C=20=EB=B0=9C=EC=86=A1=20(?= =?UTF-8?q?=EC=9E=90=EA=B8=B0=20=EB=B0=98=EC=9D=91=20=EC=A0=9C=EC=99=B8)?= =?UTF-8?q?=20-=20=EA=B3=B5=EC=9C=A0=20=EB=AC=B8=EC=A0=9C=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EC=9E=91=EC=84=B1=20=EC=8B=9C=20=EB=AC=B8=EC=A0=9C?= =?UTF-8?q?=20=EA=B3=B5=EC=9C=A0=EC=9E=90=EC=97=90=EA=B2=8C=20=EB=B0=9C?= =?UTF-8?q?=EC=86=A1=20(=EC=9E=90=EA=B8=B0=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8,=2050=EC=9E=90=20=EB=AF=B8=EB=A6=AC=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../quartz/ChallengeNotificationJob.java | 50 ++++++++++++ .../ChallengeNotificationScheduler.java | 80 +++++++++++++++++++ .../service/StudyRoomChallengeService.java | 26 ++++++ .../StudyRoomSharedProblemCommentService.java | 19 +++++ .../StudyRoomSharedProblemService.java | 51 +++++++++++- 5 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/quartz/ChallengeNotificationJob.java create mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/quartz/ChallengeNotificationScheduler.java diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/quartz/ChallengeNotificationJob.java b/src/main/java/com/aisip/OnO/backend/studyroom/quartz/ChallengeNotificationJob.java new file mode 100644 index 00000000..4eb761e7 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/quartz/ChallengeNotificationJob.java @@ -0,0 +1,50 @@ +package com.aisip.OnO.backend.studyroom.quartz; + +import com.aisip.OnO.backend.studyroom.repository.StudyRoomMemberRepository; +import com.aisip.OnO.backend.util.fcm.dto.NotificationRequestDto; +import com.aisip.OnO.backend.util.fcm.service.FcmService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ChallengeNotificationJob implements Job { + + private final StudyRoomMemberRepository memberRepository; + private final FcmService fcmService; + + @Override + public void execute(JobExecutionContext context) { + JobDataMap dataMap = context.getMergedJobDataMap(); + Long roomId = Long.parseLong(dataMap.getString("roomId")); + String challengeTitle = dataMap.getString("challengeTitle"); + String notificationType = dataMap.getString("notificationType"); + + String title, body; + if ("HALFWAY".equals(notificationType)) { + title = "챌린지 중간 알림"; + body = "'" + challengeTitle + "' 챌린지가 절반 지났어요! 목표를 향해 달려가세요."; + } else { + title = "챌린지 마감 D-1"; + body = "'" + challengeTitle + "' 챌린지가 내일 마감돼요. 마무리 스퍼트!"; + } + + NotificationRequestDto dto = new NotificationRequestDto(null, title, body, + Map.of("type", "CHALLENGE_NOTIFICATION", "roomId", String.valueOf(roomId))); + + memberRepository.findAllWithUserByRoomId(roomId).forEach(member -> { + try { + fcmService.sendNotificationToAllUserDevice(member.getUser().getId(), dto); + } catch (Exception e) { + log.warn("챌린지 알림 발송 실패 - userId: {}", member.getUser().getId(), e); + } + }); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/quartz/ChallengeNotificationScheduler.java b/src/main/java/com/aisip/OnO/backend/studyroom/quartz/ChallengeNotificationScheduler.java new file mode 100644 index 00000000..5f3b15f5 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/studyroom/quartz/ChallengeNotificationScheduler.java @@ -0,0 +1,80 @@ +package com.aisip.OnO.backend.studyroom.quartz; + +import com.aisip.OnO.backend.studyroom.entity.StudyRoomChallenge; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.quartz.*; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ChallengeNotificationScheduler { + + private static final String GROUP = "CHALLENGE_NOTIFICATION"; + private static final ZoneId ZONE = ZoneId.of("Asia/Seoul"); + + private final Scheduler scheduler; + + public void scheduleNotifications(StudyRoomChallenge challenge) { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime startAt = challenge.getStartAt(); + LocalDateTime endAt = challenge.getEndAt(); + + LocalDateTime halfwayAt = startAt.plus(Duration.between(startAt, endAt).dividedBy(2)); + if (halfwayAt.isAfter(now)) { + schedule(challenge, "HALFWAY", halfwayAt); + } + + LocalDateTime oneDayBeforeAt = endAt.minusDays(1); + if (oneDayBeforeAt.isAfter(now)) { + schedule(challenge, "ONE_DAY_LEFT", oneDayBeforeAt); + } + } + + public void cancelNotifications(Long challengeId) { + cancelJob(challengeId, "HALFWAY"); + cancelJob(challengeId, "ONE_DAY_LEFT"); + } + + private void schedule(StudyRoomChallenge challenge, String type, LocalDateTime fireAt) { + try { + String key = "challenge-" + challenge.getId() + "-" + type; + JobDataMap dataMap = new JobDataMap(); + dataMap.put("roomId", String.valueOf(challenge.getRoom().getId())); + dataMap.put("challengeTitle", challenge.getTitle()); + dataMap.put("notificationType", type); + + JobDetail jobDetail = JobBuilder.newJob(ChallengeNotificationJob.class) + .withIdentity(key, GROUP) + .usingJobData(dataMap) + .build(); + + Trigger trigger = TriggerBuilder.newTrigger() + .withIdentity(key, GROUP) + .startAt(Date.from(fireAt.atZone(ZONE).toInstant())) + .withSchedule(SimpleScheduleBuilder.simpleSchedule()) + .build(); + + scheduler.scheduleJob(jobDetail, trigger); + log.info("챌린지 알림 등록 - challengeId: {}, type: {}, fireAt: {}", challenge.getId(), type, fireAt); + } catch (SchedulerException e) { + log.warn("챌린지 알림 등록 실패 - challengeId: {}, type: {}", challenge.getId(), type, e); + } + } + + private void cancelJob(Long challengeId, String type) { + try { + String key = "challenge-" + challengeId + "-" + type; + scheduler.unscheduleJob(TriggerKey.triggerKey(key, GROUP)); + scheduler.deleteJob(JobKey.jobKey(key, GROUP)); + } catch (SchedulerException e) { + log.warn("챌린지 알림 취소 실패 - challengeId: {}, type: {}", challengeId, type, e); + } + } +} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java index 5dac61cc..75cd7c00 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java @@ -5,9 +5,13 @@ import com.aisip.OnO.backend.studyroom.dto.StudyRoomStats; import com.aisip.OnO.backend.studyroom.entity.*; import com.aisip.OnO.backend.studyroom.exception.StudyRoomErrorCase; +import com.aisip.OnO.backend.studyroom.quartz.ChallengeNotificationScheduler; import com.aisip.OnO.backend.studyroom.repository.StudyRoomChallengeRepository; import com.aisip.OnO.backend.studyroom.repository.StudyRoomMemberRepository; +import com.aisip.OnO.backend.util.fcm.dto.NotificationRequestDto; +import com.aisip.OnO.backend.util.fcm.service.FcmService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -18,6 +22,7 @@ import java.util.Locale; import java.util.Map; +@Slf4j @Service @RequiredArgsConstructor public class StudyRoomChallengeService { @@ -28,6 +33,8 @@ public class StudyRoomChallengeService { private final StudyRoomChallengeRepository challengeRepository; private final StudyRoomMemberRepository memberRepository; private final StudyRoomStatsService statsService; + private final ChallengeNotificationScheduler notificationScheduler; + private final FcmService fcmService; @Transactional public List getChallenges(Long roomId, Long userId) { @@ -64,6 +71,7 @@ public ChallengeResponse createChallenge(Long roomId, Long userId, ChallengeCrea startAt, request.endAt() )); + notificationScheduler.scheduleNotifications(challenge); return toResponse(challenge, memberRepository.findAllWithUserByRoomId(roomId), true); } @@ -72,6 +80,7 @@ public void deleteChallenge(Long roomId, Long challengeId, Long userId) { accessService.validateHost(roomId, userId); StudyRoomChallenge challenge = challengeRepository.findByIdAndRoomId(challengeId, roomId) .orElseThrow(() -> new ApplicationException(StudyRoomErrorCase.CHALLENGE_NOT_FOUND)); + notificationScheduler.cancelNotifications(challengeId); challengeRepository.delete(challenge); } @@ -109,6 +118,9 @@ private ChallengeResponse toResponse(StudyRoomChallenge challenge, List members) { + NotificationRequestDto dto = new NotificationRequestDto(null, + "챌린지 달성! 🎉", + "'" + challenge.getTitle() + "' 챌린지를 달성했어요!", + Map.of("type", "CHALLENGE_COMPLETED", "roomId", String.valueOf(challenge.getRoom().getId()))); + members.forEach(member -> { + try { + fcmService.sendNotificationToAllUserDevice(member.getUser().getId(), dto); + } catch (Exception e) { + log.warn("챌린지 달성 알림 발송 실패 - userId: {}", member.getUser().getId(), e); + } + }); + } + private LocalDate min(LocalDate left, LocalDate right) { return left.isBefore(right) ? left : right; } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemCommentService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemCommentService.java index c0a5df9f..6340051d 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemCommentService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemCommentService.java @@ -16,7 +16,10 @@ import com.aisip.OnO.backend.user.entity.User; import com.aisip.OnO.backend.user.exception.UserErrorCase; import com.aisip.OnO.backend.user.repository.UserRepository; +import com.aisip.OnO.backend.util.fcm.dto.NotificationRequestDto; +import com.aisip.OnO.backend.util.fcm.service.FcmService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,6 +29,7 @@ import java.util.Map; import java.util.stream.Collectors; +@Slf4j @Service @RequiredArgsConstructor public class StudyRoomSharedProblemCommentService { @@ -39,6 +43,7 @@ public class StudyRoomSharedProblemCommentService { private final UserRepository userRepository; private final StudyRoomReactionService reactionService; private final CustomEmojiValidator customEmojiValidator; + private final FcmService fcmService; @Transactional(readOnly = true) public CursorPageResponse getComments(Long roomId, Long sharedProblemId, @@ -75,6 +80,20 @@ public SharedProblemCommentResponse createComment(Long roomId, Long sharedProble .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); StudyRoomSharedProblemComment comment = commentRepository.save( StudyRoomSharedProblemComment.create(sharedProblem, user, validateContent(request))); + Long sharerId = sharedProblem.getSharedByUser().getId(); + if (!sharerId.equals(userId)) { + String preview = comment.getContent().length() > 50 + ? comment.getContent().substring(0, 50) + "…" + : comment.getContent(); + try { + fcmService.sendNotificationToAllUserDevice(sharerId, new NotificationRequestDto(null, + "공유 문제에 댓글이 달렸어요", + user.getName() + ": " + preview, + Map.of("type", "SHARED_PROBLEM_COMMENT", "roomId", String.valueOf(roomId), "sharedProblemId", String.valueOf(sharedProblemId)))); + } catch (Exception e) { + log.warn("댓글 알림 발송 실패 - userId: {}", sharerId, e); + } + } return toResponse(comment, List.of(), userId, false); } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java index 8b62e288..0d229945 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java @@ -15,7 +15,10 @@ import com.aisip.OnO.backend.user.entity.User; import com.aisip.OnO.backend.user.exception.UserErrorCase; import com.aisip.OnO.backend.user.repository.UserRepository; +import com.aisip.OnO.backend.util.fcm.dto.NotificationRequestDto; +import com.aisip.OnO.backend.util.fcm.service.FcmService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; @@ -25,6 +28,7 @@ import java.util.Map; import java.util.stream.Collectors; +@Slf4j @Service @RequiredArgsConstructor public class StudyRoomSharedProblemService { @@ -34,11 +38,13 @@ public class StudyRoomSharedProblemService { private final StudyRoomSharedProblemReactionRepository reactionRepository; private final StudyRoomSharedProblemCommentRepository commentRepository; private final StudyRoomSharedProblemCommentReactionRepository commentReactionRepository; + private final StudyRoomMemberRepository memberRepository; private final UserRepository userRepository; private final ProblemService problemService; private final StudyRoomReactionService reactionService; private final ApplicationEventPublisher eventPublisher; private final CustomEmojiValidator customEmojiValidator; + private final FcmService fcmService; @Transactional(readOnly = true) public CursorPageResponse getSharedProblems(Long roomId, Long userId, Long cursor, int size) { @@ -88,6 +94,10 @@ public SharedProblemResponse shareProblem(Long roomId, Long userId, SharedProble eventPublisher.publishEvent(new StudyRoomActivityEvent(userId, StudyRoomFeedEventType.PROBLEM_SHARED, Map.of("reference", problem.getReference() == null ? "" : problem.getReference(), "sharedProblemId", sharedProblem.getId()))); + notifyRoomMembers(roomId, userId, + user.getName() + "님이 문제를 공유했어요", + referenceOrFallback(problem.getReference()), + Map.of("type", "SHARED_PROBLEM", "roomId", String.valueOf(roomId), "sharedProblemId", String.valueOf(sharedProblem.getId()))); return toResponse(sharedProblem, List.of(), 0L, userId); } @@ -99,9 +109,22 @@ public SharedProblemReactionToggleResponse toggleReaction(Long roomId, Long shar .orElseThrow(() -> new ApplicationException(StudyRoomErrorCase.SHARED_PROBLEM_NOT_FOUND)); User user = userRepository.findById(userId) .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); - reactionRepository.findBySharedProblemIdAndUserIdAndEmoji(sharedProblemId, userId, request.emoji()) - .ifPresentOrElse(reactionRepository::delete, - () -> reactionRepository.save(StudyRoomSharedProblemReaction.create(sharedProblem, user, request.emoji()))); + boolean added; + var existing = reactionRepository.findBySharedProblemIdAndUserIdAndEmoji(sharedProblemId, userId, request.emoji()); + if (existing.isPresent()) { + reactionRepository.delete(existing.get()); + added = false; + } else { + reactionRepository.save(StudyRoomSharedProblemReaction.create(sharedProblem, user, request.emoji())); + added = true; + } + Long sharerId = sharedProblem.getSharedByUser().getId(); + if (added && !sharerId.equals(userId)) { + notifyUser(sharerId, + "공유 문제에 반응이 달렸어요", + user.getName() + "님이 " + request.emoji() + " 반응을 남겼어요.", + Map.of("type", "SHARED_PROBLEM_REACTION", "roomId", String.valueOf(roomId), "sharedProblemId", String.valueOf(sharedProblemId))); + } return new SharedProblemReactionToggleResponse(sharedProblemId, reactionService.summarizeSharedProblemReactions(reactionRepository.findAllBySharedProblemId(sharedProblemId), userId)); } @@ -157,4 +180,26 @@ private String problemImageUrl(Problem problem) { .findFirst() .orElse(null); } + + private void notifyRoomMembers(Long roomId, Long excludeUserId, String title, String body, Map data) { + NotificationRequestDto dto = new NotificationRequestDto(null, title, body, data); + memberRepository.findAllWithUserByRoomId(roomId).forEach(member -> { + Long memberId = member.getUser().getId(); + if (!memberId.equals(excludeUserId)) { + notifyUser(memberId, dto); + } + }); + } + + private void notifyUser(Long userId, String title, String body, Map data) { + notifyUser(userId, new NotificationRequestDto(null, title, body, data)); + } + + private void notifyUser(Long userId, NotificationRequestDto dto) { + try { + fcmService.sendNotificationToAllUserDevice(userId, dto); + } catch (Exception e) { + log.warn("공유 문제 알림 발송 실패 - userId: {}", userId, e); + } + } } From a185f93c26acad265018cdf7dfac7128d79c08d7 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Mon, 29 Jun 2026 19:19:03 +0900 Subject: [PATCH 44/67] =?UTF-8?q?[Fix]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20QA=20=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 스터디룸 등록 시 이미지 첨부 지원: POST /api/study-room에 multipart/form-data 엔드포인트 추가, 생성 시 선택적 썸네일 업로드 가능 - FCM 이모지 반응 알림 개선: 공유 문제 반응 FCM 메시지에서 이모지 제거 ("님이 반응을 추가했어요."로 변경) - 공부중 세션 기능 폐기: StudySession 엔티티·컨트롤러·서비스·레포지토리·Quartz 잡 전체 삭제, V13 마이그레이션으로 study_session 테이블 DROP - 테스트 정합성 수정: ChallengeCreateRequest periodDays 필드 누락 수정, SESSION_STARTED 이벤트 타입 대체 --- .../controller/StudyRoomController.java | 10 +- .../controller/StudySessionController.java | 38 ----- .../backend/studyroom/dto/StudyRoomDtos.java | 13 -- .../entity/StudyRoomFeedEventType.java | 1 - .../studyroom/entity/StudySession.java | 55 ------- .../studyroom/quartz/StudyRoomScheduler.java | 16 -- .../quartz/StudySessionCleanupJob.java | 22 --- .../repository/StudySessionRepository.java | 26 ---- .../studyroom/service/StudyRoomService.java | 15 +- .../StudyRoomSharedProblemService.java | 2 +- .../service/StudySessionService.java | 70 --------- .../db/migration/V13__drop_study_session.sql | 1 + .../StudyRoomApiIntegrationTest.java | 41 +----- .../StudyRoomChallengeApiIntegrationTest.java | 10 +- .../integration/StudyRoomFeedApiTest.java | 6 +- .../integration/StudySessionApiTest.java | 138 ------------------ .../StudyRoomChallengeServiceTest.java | 28 ++-- .../service/StudyRoomFeedServiceUnitTest.java | 4 +- .../StudyRoomServiceIntegrationTest.java | 2 +- 19 files changed, 57 insertions(+), 441 deletions(-) delete mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/controller/StudySessionController.java delete mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/entity/StudySession.java delete mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudySessionCleanupJob.java delete mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/repository/StudySessionRepository.java delete mode 100644 src/main/java/com/aisip/OnO/backend/studyroom/service/StudySessionService.java create mode 100644 src/main/resources/db/migration/V13__drop_study_session.sql delete mode 100644 src/test/java/com/aisip/OnO/backend/studyroom/integration/StudySessionApiTest.java diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java index 85e26c4a..a115f5c3 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java @@ -26,12 +26,20 @@ public CommonResponse> getMyRooms() { return CommonResponse.success(studyRoomService.getMyRooms(currentUserId())); } - @PostMapping + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.CREATED) public CommonResponse createRoom(@RequestBody StudyRoomCreateRequest request) { return CommonResponse.success(studyRoomService.createRoom(request, currentUserId())); } + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @ResponseStatus(HttpStatus.CREATED) + public CommonResponse createRoomMultipart( + @RequestParam("name") String name, + @RequestPart(value = "thumbnailImage", required = false) MultipartFile thumbnailImage) { + return CommonResponse.success(studyRoomService.createRoom(name, thumbnailImage, currentUserId())); + } + @GetMapping("/{roomId}") public CommonResponse getRoom(@PathVariable("roomId") Long roomId) { return CommonResponse.success(studyRoomService.getRoom(roomId, currentUserId())); diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudySessionController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudySessionController.java deleted file mode 100644 index 9496e972..00000000 --- a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudySessionController.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.aisip.OnO.backend.studyroom.controller; - -import com.aisip.OnO.backend.common.response.CommonResponse; -import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; -import com.aisip.OnO.backend.studyroom.service.StudySessionService; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/study-room/{roomId}/sessions") -public class StudySessionController { - - private final StudySessionService sessionService; - - @GetMapping("/active") - public CommonResponse getActiveSessions(@PathVariable("roomId") Long roomId) { - return CommonResponse.success(sessionService.getActiveSessions(roomId, currentUserId())); - } - - @PostMapping - @ResponseStatus(HttpStatus.CREATED) - public CommonResponse start(@PathVariable("roomId") Long roomId) { - return CommonResponse.success(sessionService.start(roomId, currentUserId())); - } - - @PatchMapping("/{sessionId}/end") - public CommonResponse end(@PathVariable("roomId") Long roomId, - @PathVariable("sessionId") Long sessionId) { - return CommonResponse.success(sessionService.end(roomId, sessionId, currentUserId())); - } - - private Long currentUserId() { - return (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - } -} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java index fbc45a4a..c5b5d1ee 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java @@ -85,19 +85,6 @@ public record ChallengeResponse(Long challengeId, String title, String type, Str public record ChallengeMemberProgressResponse(Long userId, String name, int current, boolean cleared) { } - public record ActiveStudySessionsResponse(List activeSessions) { - } - - public record ActiveStudySessionResponse(Long userId, String name, LocalDateTime startedAt) { - } - - public record StudySessionStartResponse(Long sessionId, LocalDateTime startedAt) { - } - - public record StudySessionEndResponse(Long sessionId, LocalDateTime startedAt, LocalDateTime endedAt, - Integer durationMinutes) { - } - public record SharedProblemResponse(Long sharedProblemId, Long sharedByUserId, String sharedByName, String sharedByProfileImageUrl, Long problemId, String problemImageUrl, List problemImageUrls, String reference, diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeedEventType.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeedEventType.java index 4b1de01d..ba14e4b3 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeedEventType.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomFeedEventType.java @@ -6,6 +6,5 @@ public enum StudyRoomFeedEventType { STREAK_MILESTONE, LEVEL_UP, CHALLENGE_CLEARED, - SESSION_STARTED, PROBLEM_SHARED } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudySession.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudySession.java deleted file mode 100644 index 79ad1e97..00000000 --- a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudySession.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.aisip.OnO.backend.studyroom.entity; - -import com.aisip.OnO.backend.common.entity.BaseEntity; -import com.aisip.OnO.backend.user.entity.User; -import jakarta.persistence.*; -import lombok.*; - -import java.time.Duration; -import java.time.LocalDateTime; - -@Getter -@Entity -@Builder(access = AccessLevel.PRIVATE) -@AllArgsConstructor(access = AccessLevel.PROTECTED) -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "study_session", indexes = { - @Index(name = "idx_study_session_room_ended", columnList = "room_id, ended_at"), - @Index(name = "idx_study_session_user_ended", columnList = "user_id, ended_at") -}) -public class StudySession extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "room_id", nullable = false) - private StudyRoom room; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User user; - - @Column(name = "started_at", nullable = false) - private LocalDateTime startedAt; - - @Column(name = "ended_at") - private LocalDateTime endedAt; - - @Column(name = "duration_minutes") - private Integer durationMinutes; - - public static StudySession start(StudyRoom room, User user, LocalDateTime startedAt) { - return StudySession.builder() - .room(room) - .user(user) - .startedAt(startedAt) - .build(); - } - - public void end(LocalDateTime endedAt) { - this.endedAt = endedAt; - this.durationMinutes = Math.toIntExact(Math.max(0, Duration.between(startedAt, endedAt).toMinutes())); - } -} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudyRoomScheduler.java b/src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudyRoomScheduler.java index 40ebf410..481efa2c 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudyRoomScheduler.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudyRoomScheduler.java @@ -24,20 +24,4 @@ public Trigger studyRoomWeeklyReportTrigger() { .build(); } - @Bean - public JobDetail studySessionCleanupJobDetail() { - return JobBuilder.newJob(StudySessionCleanupJob.class) - .withIdentity("studySessionCleanupJob") - .storeDurably() - .build(); - } - - @Bean - public Trigger studySessionCleanupTrigger() { - return TriggerBuilder.newTrigger() - .forJob(studySessionCleanupJobDetail()) - .withIdentity("studySessionCleanupTrigger") - .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInHours(1).repeatForever()) - .build(); - } } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudySessionCleanupJob.java b/src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudySessionCleanupJob.java deleted file mode 100644 index d6de021b..00000000 --- a/src/main/java/com/aisip/OnO/backend/studyroom/quartz/StudySessionCleanupJob.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.aisip.OnO.backend.studyroom.quartz; - -import com.aisip.OnO.backend.studyroom.service.StudySessionService; -import lombok.RequiredArgsConstructor; -import org.quartz.Job; -import org.quartz.JobExecutionContext; -import org.springframework.stereotype.Component; - -import java.time.LocalDateTime; - -@Component -@RequiredArgsConstructor -public class StudySessionCleanupJob implements Job { - - private final StudySessionService sessionService; - - @Override - public void execute(JobExecutionContext context) { - LocalDateTime now = LocalDateTime.now(); - sessionService.closeExpiredSessions(now.minusHours(12), now); - } -} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudySessionRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudySessionRepository.java deleted file mode 100644 index 1fac3ce0..00000000 --- a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudySessionRepository.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.aisip.OnO.backend.studyroom.repository; - -import com.aisip.OnO.backend.studyroom.entity.StudySession; -import jakarta.persistence.LockModeType; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Lock; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -public interface StudySessionRepository extends JpaRepository { - - @Query("select s from StudySession s join fetch s.user where s.room.id = :roomId and s.endedAt is null order by s.startedAt asc") - List findActiveByRoomId(@Param("roomId") Long roomId); - - @Lock(LockModeType.PESSIMISTIC_WRITE) - @Query("select s from StudySession s where s.user.id = :userId and s.endedAt is null") - List findActiveByUserIdForUpdate(@Param("userId") Long userId); - - Optional findByIdAndRoomIdAndUserId(Long id, Long roomId, Long userId); - - List findAllByEndedAtIsNullAndStartedAtBefore(LocalDateTime threshold); -} diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java index f20d1fce..b2c969e2 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java @@ -73,15 +73,26 @@ public StudyRoomDetailResponse getRoom(Long roomId, Long userId) { @Transactional public StudyRoomDetailResponse createRoom(StudyRoomCreateRequest request, Long userId) { - String name = validateName(request == null ? null : request.name()); + return createRoom(request == null ? null : request.name(), null, userId); + } + + @Transactional + public StudyRoomDetailResponse createRoom(String name, MultipartFile thumbnailImage, Long userId) { + String validatedName = validateName(name); User user = userRepository.findByIdForUpdate(userId) .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); if (memberRepository.countByUserId(userId) >= MAX_USER_ROOM_COUNT) { throw new ApplicationException(StudyRoomErrorCase.STUDY_ROOM_LIMIT_EXCEEDED); } - StudyRoom room = StudyRoom.create(name, userId); + StudyRoom room = StudyRoom.create(validatedName, userId); room.addMember(StudyRoomMember.create(user, StudyRoomMemberRole.HOST)); roomRepository.save(room); + if (thumbnailImage != null && !thumbnailImage.isEmpty()) { + validateThumbnail(thumbnailImage); + String thumbnailUrl = fileUploadService.uploadFileToS3(thumbnailImage); + deleteThumbnailOnRollback(thumbnailUrl, room.getId()); + room.updateThumbnailUrl(thumbnailUrl); + } return buildDetail(room); } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java index 0d229945..126b40b5 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomSharedProblemService.java @@ -122,7 +122,7 @@ public SharedProblemReactionToggleResponse toggleReaction(Long roomId, Long shar if (added && !sharerId.equals(userId)) { notifyUser(sharerId, "공유 문제에 반응이 달렸어요", - user.getName() + "님이 " + request.emoji() + " 반응을 남겼어요.", + user.getName() + "님이 반응을 추가했어요.", Map.of("type", "SHARED_PROBLEM_REACTION", "roomId", String.valueOf(roomId), "sharedProblemId", String.valueOf(sharedProblemId))); } return new SharedProblemReactionToggleResponse(sharedProblemId, diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudySessionService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudySessionService.java deleted file mode 100644 index 4dacba71..00000000 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudySessionService.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.aisip.OnO.backend.studyroom.service; - -import com.aisip.OnO.backend.common.exception.ApplicationException; -import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; -import com.aisip.OnO.backend.studyroom.entity.StudyRoom; -import com.aisip.OnO.backend.studyroom.entity.StudyRoomFeedEventType; -import com.aisip.OnO.backend.studyroom.entity.StudySession; -import com.aisip.OnO.backend.studyroom.event.StudyRoomActivityEvent; -import com.aisip.OnO.backend.studyroom.exception.StudyRoomErrorCase; -import com.aisip.OnO.backend.studyroom.repository.StudySessionRepository; -import com.aisip.OnO.backend.user.entity.User; -import com.aisip.OnO.backend.user.exception.UserErrorCase; -import com.aisip.OnO.backend.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.util.Map; - -@Service -@RequiredArgsConstructor -public class StudySessionService { - - private final StudyRoomAccessService accessService; - private final StudySessionRepository sessionRepository; - private final UserRepository userRepository; - private final ApplicationEventPublisher eventPublisher; - - @Transactional(readOnly = true) - public ActiveStudySessionsResponse getActiveSessions(Long roomId, Long userId) { - accessService.validateMember(roomId, userId); - return new ActiveStudySessionsResponse(sessionRepository.findActiveByRoomId(roomId).stream() - .map(session -> new ActiveStudySessionResponse( - session.getUser().getId(), session.getUser().getName(), session.getStartedAt())) - .toList()); - } - - @Transactional - public StudySessionStartResponse start(Long roomId, Long userId) { - accessService.validateMember(roomId, userId); - if (!sessionRepository.findActiveByUserIdForUpdate(userId).isEmpty()) { - throw new ApplicationException(StudyRoomErrorCase.SESSION_ALREADY_ACTIVE); - } - StudyRoom room = accessService.getRoomOrThrow(roomId); - User user = userRepository.findById(userId) - .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); - StudySession session = sessionRepository.save(StudySession.start(room, user, LocalDateTime.now())); - eventPublisher.publishEvent(new StudyRoomActivityEvent(userId, StudyRoomFeedEventType.SESSION_STARTED, Map.of())); - return new StudySessionStartResponse(session.getId(), session.getStartedAt()); - } - - @Transactional - public StudySessionEndResponse end(Long roomId, Long sessionId, Long userId) { - accessService.validateMember(roomId, userId); - StudySession session = sessionRepository.findByIdAndRoomIdAndUserId(sessionId, roomId, userId) - .orElseThrow(() -> new ApplicationException(StudyRoomErrorCase.SESSION_NOT_FOUND)); - if (session.getEndedAt() == null) { - session.end(LocalDateTime.now()); - } - return new StudySessionEndResponse(session.getId(), session.getStartedAt(), session.getEndedAt(), session.getDurationMinutes()); - } - - @Transactional - public void closeExpiredSessions(LocalDateTime threshold, LocalDateTime endedAt) { - sessionRepository.findAllByEndedAtIsNullAndStartedAtBefore(threshold) - .forEach(session -> session.end(endedAt)); - } -} diff --git a/src/main/resources/db/migration/V13__drop_study_session.sql b/src/main/resources/db/migration/V13__drop_study_session.sql new file mode 100644 index 00000000..a5c32e84 --- /dev/null +++ b/src/main/resources/db/migration/V13__drop_study_session.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS study_session; diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java index 111c2fda..dec79b95 100644 --- a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomApiIntegrationTest.java @@ -257,7 +257,7 @@ void feedReactionToggleAndInvalidEmojiAreValidatedByApi() throws Exception { authenticate(host.getId()); Long roomId = createRoom("피드 반응방"); StudyRoom room = roomRepository.findById(roomId).orElseThrow(); - StudyRoomFeed feed = feedRepository.save(StudyRoomFeed.create(room, host, StudyRoomFeedEventType.SESSION_STARTED, "{}")); + StudyRoomFeed feed = feedRepository.save(StudyRoomFeed.create(room, host, StudyRoomFeedEventType.PROBLEM_REGISTERED, "{}")); mockMvc.perform(post("/api/study-room/{roomId}/feed/{feedId}/reactions", roomId, feed.getId()) .with(auth()) @@ -319,35 +319,6 @@ void sharedProblemShareReactionAndDeleteAuthorizationApiFlow() throws Exception .andExpect(jsonPath("$.data").value("공유 문제 삭제가 완료되었습니다.")); } - @Test - void studySessionStartActiveDuplicateAndEndApiFlow() throws Exception { - User host = saveUser("방장", "session-host"); - authenticate(host.getId()); - Long roomId = createRoom("세션방"); - - Long sessionId = startSession(roomId); - mockMvc.perform(get("/api/study-room/{roomId}/sessions/active", roomId) - .with(auth())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.activeSessions[0].userId").value(host.getId())); - - mockMvc.perform(post("/api/study-room/{roomId}/sessions", roomId) - .with(auth())) - .andExpect(status().isConflict()) - .andExpect(jsonPath("$.errorCode").value(10011)); - - mockMvc.perform(patch("/api/study-room/{roomId}/sessions/{sessionId}/end", roomId, sessionId) - .with(auth())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.sessionId").value(sessionId)) - .andExpect(jsonPath("$.data.endedAt").exists()); - - mockMvc.perform(get("/api/study-room/{roomId}/sessions/active", roomId) - .with(auth())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.activeSessions").isEmpty()); - } - @Test void challengeApiUsesCurrentPeriodPracticeCountsAndKeepsPeriodChallengeInProgress() throws Exception { User host = saveUser("방장", "challenge-host"); @@ -364,6 +335,7 @@ void challengeApiUsesCurrentPeriodPracticeCountsAndKeepsPeriodChallengeInProgres "individual", "practice_count", "weekly", + null, 2, startAt, endAt @@ -389,6 +361,7 @@ void challengeApiUsesCurrentPeriodPracticeCountsAndKeepsPeriodChallengeInProgres "individual", "practice_count", "weekly", + null, 2, startAt, endAt @@ -446,14 +419,6 @@ private void join(String inviteCode) throws Exception { .andExpect(status().isOk()); } - private Long startSession(Long roomId) throws Exception { - MvcResult result = mockMvc.perform(post("/api/study-room/{roomId}/sessions", roomId) - .with(auth())) - .andExpect(status().isCreated()) - .andReturn(); - return readLong(result, "$.data.sessionId"); - } - private Long shareProblem(Long roomId, Long problemId, String comment) throws Exception { MvcResult result = mockMvc.perform(post("/api/study-room/{roomId}/shared-problems", roomId) .with(auth()) diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomChallengeApiIntegrationTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomChallengeApiIntegrationTest.java index a426303b..9dadf454 100644 --- a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomChallengeApiIntegrationTest.java +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomChallengeApiIntegrationTest.java @@ -99,6 +99,7 @@ void memberCanCreateWeeklyChallengeAndProgressCountsOnlyCurrentPeriod() throws E "individual", "practice_count", "weekly", + null, 2, startAt, startAt.plusWeeks(3) @@ -144,6 +145,7 @@ void createChallengeRejectsInvalidPeriod() throws Exception { "individual", "practice_count", "yearly", + null, 1, null, LocalDateTime.now().plusDays(1) @@ -161,7 +163,7 @@ void createChallengeRejectsInvalidPeriod() throws Exception { void hostCanDeleteChallengeAndMemberCannot() throws Exception { ChallengeCreateRequest request = new ChallengeCreateRequest( "삭제 테스트", "individual", "practice_count", null, - 3, null, LocalDateTime.now().plusDays(7) + null, 3, null, LocalDateTime.now().plusDays(7) ); // 방장으로 챌린지 생성: 세션 간 인증 공유 문제를 피하기 위해 auth() RequestPostProcessor 사용 MvcResult createResult = mockMvc.perform(post("/api/study-room/{roomId}/challenges", roomId) @@ -193,7 +195,7 @@ void hostCanDeleteChallengeAndMemberCannot() throws Exception { void createChallengeRejectsTitleTooLong() throws Exception { ChallengeCreateRequest request = new ChallengeCreateRequest( "a".repeat(41), "individual", "practice_count", null, - 3, null, LocalDateTime.now().plusDays(7) + null, 3, null, LocalDateTime.now().plusDays(7) ); authenticate(member.getId()); mockMvc.perform(post("/api/study-room/{roomId}/challenges", roomId) @@ -207,7 +209,7 @@ void createChallengeRejectsTitleTooLong() throws Exception { void createChallengeRejectsPastEndAt() throws Exception { ChallengeCreateRequest request = new ChallengeCreateRequest( "과거 종료일", "individual", "practice_count", null, - 3, null, LocalDateTime.now().minusDays(1) + null, 3, null, LocalDateTime.now().minusDays(1) ); authenticate(member.getId()); mockMvc.perform(post("/api/study-room/{roomId}/challenges", roomId) @@ -221,7 +223,7 @@ void createChallengeRejectsPastEndAt() throws Exception { void createChallengeRejectsStartAtAfterEndAt() throws Exception { ChallengeCreateRequest request = new ChallengeCreateRequest( "날짜 역전", "individual", "practice_count", null, - 3, LocalDateTime.now().plusDays(5), LocalDateTime.now().plusDays(3) + null, 3, LocalDateTime.now().plusDays(5), LocalDateTime.now().plusDays(3) ); authenticate(member.getId()); mockMvc.perform(post("/api/study-room/{roomId}/challenges", roomId) diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomFeedApiTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomFeedApiTest.java index 3c44ac02..007a80cd 100644 --- a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomFeedApiTest.java +++ b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudyRoomFeedApiTest.java @@ -85,7 +85,7 @@ void feedPaginationWithCursor() throws Exception { // 피드 5개 직접 저장 (DB에 직접 저장 → 연속 ID 보장) StudyRoom room = roomRepository.findById(roomId).orElseThrow(); for (int i = 0; i < 5; i++) { - feedRepository.save(StudyRoomFeed.create(room, host, StudyRoomFeedEventType.SESSION_STARTED, "{}")); + feedRepository.save(StudyRoomFeed.create(room, host, StudyRoomFeedEventType.PROBLEM_REGISTERED, "{}")); } // 첫 번째 페이지: size=3 → 3개, hasNext=true @@ -128,7 +128,7 @@ void multipleUsersCanReactToSameFeed() throws Exception { // 피드 1개 직접 저장 StudyRoom room = roomRepository.findById(roomId).orElseThrow(); StudyRoomFeed feed = feedRepository.save( - StudyRoomFeed.create(room, host, StudyRoomFeedEventType.SESSION_STARTED, "{}")); + StudyRoomFeed.create(room, host, StudyRoomFeedEventType.PROBLEM_REGISTERED, "{}")); // host가 반응 추가 → count=1 authenticate(host.getId()); @@ -160,7 +160,7 @@ void nonMemberCannotToggleFeedReaction() throws Exception { // 피드 1개 직접 저장 StudyRoom room = roomRepository.findById(roomId).orElseThrow(); StudyRoomFeed feed = feedRepository.save( - StudyRoomFeed.create(room, host, StudyRoomFeedEventType.SESSION_STARTED, "{}")); + StudyRoomFeed.create(room, host, StudyRoomFeedEventType.PROBLEM_REGISTERED, "{}")); // 외부인이 반응 시도 → 403 (10002) authenticate(outsider.getId()); diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudySessionApiTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudySessionApiTest.java deleted file mode 100644 index d3dfe068..00000000 --- a/src/test/java/com/aisip/OnO/backend/studyroom/integration/StudySessionApiTest.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.aisip.OnO.backend.studyroom.integration; - -import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; -import com.aisip.OnO.backend.user.entity.User; -import com.aisip.OnO.backend.user.repository.UserRepository; -import com.aisip.OnO.backend.util.RandomUserGenerator; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.jayway.jsonpath.JsonPath; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.request.RequestPostProcessor; - -import java.util.List; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@AutoConfigureMockMvc -@ActiveProfiles("test") -class StudySessionApiTest { - - @Autowired - private MockMvc mockMvc; - - @Autowired - private ObjectMapper objectMapper; - - @Autowired - private UserRepository userRepository; - - private Long authenticatedUserId; - - @AfterEach - void tearDown() { - SecurityContextHolder.clearContext(); - } - - @Test - void nonMemberCannotStartStudySession() throws Exception { - User host = saveUser("방장", "session-nonmember-host"); - User outsider = saveUser("외부", "session-nonmember-outsider"); - authenticate(host.getId()); - Long roomId = createRoom("세션 권한 테스트방"); - - authenticate(outsider.getId()); - mockMvc.perform(post("/api/study-room/{roomId}/sessions", roomId) - .with(auth())) - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.errorCode").value(10002)); - } - - @Test - void cannotEndOtherUsersStudySession() throws Exception { - User host = saveUser("방장", "session-end-host"); - User member = saveUser("멤버", "session-end-member"); - authenticate(host.getId()); - Long roomId = createRoom("세션 종료 테스트방"); - String inviteCode = issueInviteCode(roomId); - authenticate(member.getId()); - join(inviteCode); - - // host가 세션 시작 - authenticate(host.getId()); - MvcResult startResult = mockMvc.perform(post("/api/study-room/{roomId}/sessions", roomId) - .with(auth())) - .andExpect(status().isCreated()) - .andReturn(); - Number sessionIdValue = JsonPath.read(startResult.getResponse().getContentAsString(), "$.data.sessionId"); - Long sessionId = sessionIdValue.longValue(); - - // member가 host 세션 종료 시도 → SESSION_NOT_FOUND (10012): findByIdAndRoomIdAndUserId 미일치 - authenticate(member.getId()); - mockMvc.perform(patch("/api/study-room/{roomId}/sessions/{sessionId}/end", roomId, sessionId) - .with(auth())) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.errorCode").value(10012)); - } - - // ========== 헬퍼 ========== - - private User saveUser(String name, String emailPrefix) { - return userRepository.save(RandomUserGenerator.createRandomUser("GOOGLE", name, emailPrefix)); - } - - private void authenticate(Long userId) { - authenticatedUserId = userId; - SecurityContextHolder.getContext().setAuthentication( - new UsernamePasswordAuthenticationToken(userId, null, List.of(new SimpleGrantedAuthority("ROLE_MEMBER"))) - ); - } - - private RequestPostProcessor auth() { - return authentication(new UsernamePasswordAuthenticationToken( - authenticatedUserId, null, List.of(new SimpleGrantedAuthority("ROLE_MEMBER")) - )); - } - - private Long createRoom(String name) throws Exception { - MvcResult result = mockMvc.perform(post("/api/study-room") - .with(auth()) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(new StudyRoomCreateRequest(name)))) - .andExpect(status().isCreated()) - .andReturn(); - Number value = JsonPath.read(result.getResponse().getContentAsString(), "$.data.roomId"); - return value.longValue(); - } - - private String issueInviteCode(Long roomId) throws Exception { - MvcResult result = mockMvc.perform(post("/api/study-room/{roomId}/invite", roomId) - .with(auth())) - .andExpect(status().isOk()) - .andReturn(); - return JsonPath.read(result.getResponse().getContentAsString(), "$.data.code"); - } - - private void join(String inviteCode) throws Exception { - mockMvc.perform(post("/api/study-room/join") - .with(auth()) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(new StudyRoomJoinRequest(inviteCode)))) - .andExpect(status().isOk()); - } -} diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeServiceTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeServiceTest.java index 4c6bc129..e80aabba 100644 --- a/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeServiceTest.java +++ b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeServiceTest.java @@ -3,10 +3,12 @@ import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.ChallengeResponse; import com.aisip.OnO.backend.studyroom.dto.StudyRoomStats; import com.aisip.OnO.backend.studyroom.entity.*; +import com.aisip.OnO.backend.studyroom.quartz.ChallengeNotificationScheduler; import com.aisip.OnO.backend.studyroom.repository.StudyRoomChallengeRepository; import com.aisip.OnO.backend.studyroom.repository.StudyRoomMemberRepository; import com.aisip.OnO.backend.user.entity.User; import com.aisip.OnO.backend.util.RandomUserGenerator; +import com.aisip.OnO.backend.util.fcm.service.FcmService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -41,11 +43,17 @@ class StudyRoomChallengeServiceTest { @Mock private StudyRoomStatsService statsService; + @Mock + private ChallengeNotificationScheduler notificationScheduler; + + @Mock + private FcmService fcmService; + private StudyRoomChallengeService challengeService; @BeforeEach void setUp() { - challengeService = new StudyRoomChallengeService(accessService, challengeRepository, memberRepository, statsService); + challengeService = new StudyRoomChallengeService(accessService, challengeRepository, memberRepository, statsService, notificationScheduler, fcmService); } @Test @@ -64,7 +72,7 @@ void weeklyPeriodChallengeUsesCurrentPeriodRangeAndDoesNotCompleteEarly() { ); given(memberRepository.findAllWithUserByRoomId(10L)).willReturn(List.of(member)); - given(challengeRepository.findActiveByRoomId(eq(10L), eq(StudyRoomChallengeStatus.IN_PROGRESS), any(LocalDateTime.class))) + given(challengeRepository.findAllByRoomIdOrderByEndAtAsc(10L)) .willReturn(List.of(challenge)); given(statsService.currentStreaks(anyList(), any(), any())).willReturn(Map.of(user.getId(), 0)); given(statsService.getStats(anyList(), any(LocalDateTime.class), any(LocalDateTime.class), anyMap())) @@ -103,7 +111,7 @@ void nonPeriodGroupChallengeCompletesWhenGroupCurrentReachesTarget() { given(memberRepository.findAllWithUserByRoomId(10L)) .willReturn(List.of(StudyRoomMember.create(first, StudyRoomMemberRole.HOST), StudyRoomMember.create(second, StudyRoomMemberRole.MEMBER))); - given(challengeRepository.findActiveByRoomId(eq(10L), eq(StudyRoomChallengeStatus.IN_PROGRESS), any(LocalDateTime.class))) + given(challengeRepository.findAllByRoomIdOrderByEndAtAsc(10L)) .willReturn(List.of(challenge)); given(statsService.currentStreaks(anyList(), any(), any())).willReturn(Map.of(first.getId(), 0, second.getId(), 0)); given(statsService.getStats(anyList(), any(LocalDateTime.class), any(LocalDateTime.class), anyMap())) @@ -135,7 +143,7 @@ void dailyPeriodChallengeAggregatesCurrentDayWindow() { ); given(memberRepository.findAllWithUserByRoomId(10L)).willReturn(List.of(member)); - given(challengeRepository.findActiveByRoomId(eq(10L), eq(StudyRoomChallengeStatus.IN_PROGRESS), any(LocalDateTime.class))) + given(challengeRepository.findAllByRoomIdOrderByEndAtAsc(10L)) .willReturn(List.of(challenge)); given(statsService.currentStreaks(anyList(), any(), any())).willReturn(Map.of(user.getId(), 0)); // practice_count=1, target=2 → cleared=false @@ -180,7 +188,7 @@ void individualChallengeStaysInProgressWhenNotAllMembersCleared() { StudyRoomMember.create(member1, StudyRoomMemberRole.HOST), StudyRoomMember.create(member2, StudyRoomMemberRole.MEMBER) )); - given(challengeRepository.findActiveByRoomId(eq(10L), eq(StudyRoomChallengeStatus.IN_PROGRESS), any(LocalDateTime.class))) + given(challengeRepository.findAllByRoomIdOrderByEndAtAsc(10L)) .willReturn(List.of(challenge)); given(statsService.currentStreaks(anyList(), any(), any())) .willReturn(Map.of(member1.getId(), 0, member2.getId(), 0)); @@ -221,7 +229,7 @@ void expiredEndAtChallengeReturnsExpiredStatus() { given(memberRepository.findAllWithUserByRoomId(10L)) .willReturn(List.of(StudyRoomMember.create(user, StudyRoomMemberRole.HOST))); - given(challengeRepository.findActiveByRoomId(eq(10L), eq(StudyRoomChallengeStatus.IN_PROGRESS), any(LocalDateTime.class))) + given(challengeRepository.findAllByRoomIdOrderByEndAtAsc(10L)) .willReturn(List.of(challenge)); given(statsService.currentStreaks(anyList(), any(), any())).willReturn(Map.of(user.getId(), 0)); // practice_count=1, target=10 → 미달성이고 endAt도 과거 @@ -249,7 +257,7 @@ void attendanceMetricUsesAttendanceDayCountFromStatsService() { given(memberRepository.findAllWithUserByRoomId(10L)) .willReturn(List.of(StudyRoomMember.create(user, StudyRoomMemberRole.HOST))); - given(challengeRepository.findActiveByRoomId(eq(10L), eq(StudyRoomChallengeStatus.IN_PROGRESS), any(LocalDateTime.class))) + given(challengeRepository.findAllByRoomIdOrderByEndAtAsc(10L)) .willReturn(List.of(challenge)); given(statsService.currentStreaks(anyList(), any(), any())).willReturn(Map.of(user.getId(), 0)); given(statsService.getStats(anyList(), any(LocalDateTime.class), any(LocalDateTime.class), anyMap())) @@ -285,7 +293,7 @@ void monthlyPeriodChallengeAggregatesCurrentMonthWindow() { ); given(memberRepository.findAllWithUserByRoomId(10L)).willReturn(List.of(member)); - given(challengeRepository.findActiveByRoomId(eq(10L), eq(StudyRoomChallengeStatus.IN_PROGRESS), any(LocalDateTime.class))) + given(challengeRepository.findAllByRoomIdOrderByEndAtAsc(10L)) .willReturn(List.of(challenge)); given(statsService.currentStreaks(anyList(), any(), any())).willReturn(Map.of(user.getId(), 0)); // practice_count=1, target=3 → 미달성 → status = in_progress @@ -325,7 +333,7 @@ void streakMetricUsesAttendanceDayCount() { ); given(memberRepository.findAllWithUserByRoomId(10L)).willReturn(List.of(member)); - given(challengeRepository.findActiveByRoomId(eq(10L), eq(StudyRoomChallengeStatus.IN_PROGRESS), any(LocalDateTime.class))) + given(challengeRepository.findAllByRoomIdOrderByEndAtAsc(10L)) .willReturn(List.of(challenge)); given(statsService.currentStreaks(anyList(), any(), any())).willReturn(Map.of(user.getId(), 7)); given(statsService.getStats(anyList(), any(LocalDateTime.class), any(LocalDateTime.class), anyMap())) @@ -350,7 +358,7 @@ private StudyRoomChallenge challenge(StudyRoomChallengeType type, StudyRoomChall StudyRoom room = StudyRoom.create("테스트방", 1L); setField(room, "id", 10L); StudyRoomChallenge challenge = StudyRoomChallenge.create(room, "테스트 챌린지", type, metric, period, - targetValue, startAt, endAt); + null, targetValue, startAt, endAt); setField(challenge, "id", 100L); return challenge; } diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedServiceUnitTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedServiceUnitTest.java index 93601b21..12685984 100644 --- a/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedServiceUnitTest.java +++ b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedServiceUnitTest.java @@ -115,8 +115,8 @@ void problemRegisteredCreatesNewFeedWhenNoSameDayFeedExists() { void nonProblemRegisteredEventAlwaysCreatesNewFeed() { given(feedRepository.save(any())).willAnswer(inv -> inv.getArgument(0)); - // SESSION_STARTED는 당일 중복 체크 없이 항상 새 피드 생성 - feedService.createFeedsForUserRooms(1L, StudyRoomFeedEventType.SESSION_STARTED, Map.of()); + // PROBLEM_REGISTERED 외 이벤트는 당일 중복 체크 없이 항상 새 피드 생성 + feedService.createFeedsForUserRooms(1L, StudyRoomFeedEventType.PRACTICE_COMPLETED, Map.of()); // 새 피드가 저장되어야 함 verify(feedRepository).save(any(StudyRoomFeed.class)); diff --git a/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java index 290a11f6..2091859e 100644 --- a/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java +++ b/src/test/java/com/aisip/OnO/backend/studyroom/service/StudyRoomServiceIntegrationTest.java @@ -126,7 +126,7 @@ void feedReactionCanBeRecreatedAfterDelete() { StudyRoomFeed feed = feedRepository.save(StudyRoomFeed.create( roomRepository.findById(room.roomId()).orElseThrow(), host, - StudyRoomFeedEventType.SESSION_STARTED, + StudyRoomFeedEventType.PROBLEM_REGISTERED, "{}" )); StudyRoomFeedReaction reaction = feedReactionRepository.save(StudyRoomFeedReaction.create(feed, host, "fired_up_sparkle_eyes")); From b63edc1805b6b3e93b3bfa58e5cfba4029c30901 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Mon, 29 Jun 2026 19:29:05 +0900 Subject: [PATCH 45/67] =?UTF-8?q?[Fix]=20SESSION=5FSTARTED=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=20=EA=B8=B0=EC=A1=B4=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - V14: study_room_feed 테이블에서 event_type='SESSION_STARTED' 행 전체 삭제 - 공부중 세션 기능 폐기 후 enum에서 SESSION_STARTED 제거로 발생하는 500 오류 해소 --- .../resources/db/migration/V14__delete_session_started_feeds.sql | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/main/resources/db/migration/V14__delete_session_started_feeds.sql diff --git a/src/main/resources/db/migration/V14__delete_session_started_feeds.sql b/src/main/resources/db/migration/V14__delete_session_started_feeds.sql new file mode 100644 index 00000000..9a56a628 --- /dev/null +++ b/src/main/resources/db/migration/V14__delete_session_started_feeds.sql @@ -0,0 +1 @@ +DELETE FROM study_room_feed WHERE event_type = 'SESSION_STARTED'; From b7265b563646bdefdc7a8da859c0280f97502271 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Mon, 29 Jun 2026 20:45:24 +0900 Subject: [PATCH 46/67] =?UTF-8?q?[Fix]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20image/jpg=20content=20type=20=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - isAllowedImageType, hasAllowedImageSignature에 image/jpg 케이스 추가 - 모바일(Flutter)에서 비표준 MIME 타입 image/jpg로 전송 시 400 오류 해소 --- .../aisip/OnO/backend/studyroom/service/StudyRoomService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java index b2c969e2..ba2012bf 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java @@ -246,7 +246,7 @@ private boolean isAllowedImageType(String contentType, String extension) { return false; } return switch (contentType) { - case "image/jpeg" -> extension.equals("jpg") || extension.equals("jpeg"); + case "image/jpeg", "image/jpg" -> extension.equals("jpg") || extension.equals("jpeg"); case "image/png" -> extension.equals("png"); case "image/webp" -> extension.equals("webp"); default -> false; @@ -263,7 +263,7 @@ private boolean hasAllowedImageSignature(MultipartFile thumbnail, String content } return switch (contentType) { - case "image/jpeg" -> read >= 3 + case "image/jpeg", "image/jpg" -> read >= 3 && (header[0] & 0xFF) == 0xFF && (header[1] & 0xFF) == 0xD8 && (header[2] & 0xFF) == 0xFF; From 2f42078ad97aa27ca67960b68d10b6319868212d Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Mon, 29 Jun 2026 21:29:17 +0900 Subject: [PATCH 47/67] =?UTF-8?q?[Fix]=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=8B=9C=20application/octet-str?= =?UTF-8?q?eam=20=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit image_cropper 등 모바일 라이브러리가 MIME 타입을 application/octet-stream으로 전송하는 경우 extension 기반으로 실제 이미지 타입을 추론하도록 수정. magic bytes 검증은 그대로 유지하여 보안 수준은 동일하게 유지됨. Co-Authored-By: Claude Sonnet 4.6 --- .../studyroom/service/StudyRoomService.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java index ba2012bf..1e1c53cb 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java @@ -224,7 +224,7 @@ private void validateThumbnail(MultipartFile thumbnail) { } String extension = getLowercaseExtension(thumbnail.getOriginalFilename()); - String contentType = thumbnail.getContentType(); + String contentType = normalizeContentType(thumbnail.getContentType(), extension); if (!isAllowedImageType(contentType, extension) || !hasAllowedImageSignature(thumbnail, contentType)) { throw new ApplicationException(FileUploadErrorCase.INVALID_IMAGE_FILE); } @@ -241,6 +241,18 @@ private String getLowercaseExtension(String originalFilename) { return originalFilename.substring(dotIndex + 1).toLowerCase(Locale.ROOT); } + private String normalizeContentType(String contentType, String extension) { + if (contentType == null || "application/octet-stream".equals(contentType)) { + return switch (extension) { + case "jpg", "jpeg" -> "image/jpeg"; + case "png" -> "image/png"; + case "webp" -> "image/webp"; + default -> contentType != null ? contentType : ""; + }; + } + return contentType; + } + private boolean isAllowedImageType(String contentType, String extension) { if (contentType == null) { return false; From c0eab09a782a02beef183e4bb00a6bc58c69e610 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Mon, 29 Jun 2026 21:39:12 +0900 Subject: [PATCH 48/67] =?UTF-8?q?[Chore]=20=EB=B3=B5=EC=8A=B5=EB=85=B8?= =?UTF-8?q?=ED=8A=B8=20->=20=20=EB=B3=B5=EC=8A=B5=20=EC=84=B8=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20=EB=AA=85=EC=B9=AD=20=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OnO/backend/practicenote/service/PracticeNoteService.java | 2 +- .../backend/practicenote/service/PracticeNotificationJob.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java b/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java index 666657f5..9e77b010 100644 --- a/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java +++ b/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java @@ -44,7 +44,7 @@ @RequiredArgsConstructor @Transactional public class PracticeNoteService { - private static final String DEFAULT_PRACTICE_NOTE_TITLE = "복습노트"; + private static final String DEFAULT_PRACTICE_NOTE_TITLE = "복습 세트"; private final ProblemRepository problemRepository; diff --git a/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNotificationJob.java b/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNotificationJob.java index 595b3a83..8dfea878 100644 --- a/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNotificationJob.java +++ b/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNotificationJob.java @@ -30,7 +30,7 @@ public void executeInternal(JobExecutionContext context) { new NotificationRequestDto( "User Token", "오답노트를 복습할 시간이예요!", - practiceTitle + " 복습노트를 공부할 시간입니다!", + practiceTitle + " 복습 세트를 공부할 시간입니다!", Map.of("practiceId", String.valueOf(practiceId)) ) ); From 1b281581d6c654ffc6aa76b6c66e3d6910709230 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 30 Jun 2026 00:13:54 +0900 Subject: [PATCH 49/67] =?UTF-8?q?[Fix]=20Sentry=204xx=20ApplicationExcepti?= =?UTF-8?q?on=20=EB=85=B8=EC=9D=B4=EC=A6=88=20=ED=95=84=ED=84=B0=EB=A7=81?= =?UTF-8?q?=20BeforeSendCallback=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 4xx 상태코드의 ApplicationException은 Sentry 전송에서 제외 (유효하지 않은 토큰, 파일/문제 없음, 태그 길이 초과 등 사용자 오류는 제외) Co-Authored-By: Claude Sonnet 4.6 --- .../backend/common/config/SentryConfig.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/main/java/com/aisip/OnO/backend/common/config/SentryConfig.java diff --git a/src/main/java/com/aisip/OnO/backend/common/config/SentryConfig.java b/src/main/java/com/aisip/OnO/backend/common/config/SentryConfig.java new file mode 100644 index 00000000..e8033865 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/common/config/SentryConfig.java @@ -0,0 +1,23 @@ +package com.aisip.OnO.backend.common.config; + +import com.aisip.OnO.backend.common.exception.ApplicationException; +import io.sentry.SentryOptions; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SentryConfig { + + @Bean + public SentryOptions.BeforeSendCallback sentryBeforeSendCallback() { + return (event, hint) -> { + Throwable throwable = event.getThrowable(); + if (throwable instanceof ApplicationException appEx) { + if (appEx.getErrorCase().getHttpStatusCode() < 500) { + return null; + } + } + return event; + }; + } +} From 65f28f31f1e40f054a67d784be8d15e7a641b330 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 30 Jun 2026 00:14:56 +0900 Subject: [PATCH 50/67] =?UTF-8?q?[Fix]=20AI=20=EB=B6=84=EC=84=9D=20Deadloc?= =?UTF-8?q?k=20=EC=9C=84=ED=97=98=20=EC=99=84=ED=99=94=20-=20=ED=8A=B8?= =?UTF-8?q?=EB=9E=9C=EC=9E=AD=EC=85=98=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=8F=99=EC=8B=9C=EC=84=B1=20=EA=B0=90=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - analyzeProblemSync 트랜잭션을 데이터 조회/저장으로 분리 (OpenAI 호출 중 DB 커넥션 장기 점유 방지 → 동시 업데이트 충돌 감소) - RabbitMQ 분석 consumer 동시성 1-3 → 1-2 감소 (Deadlock 발생 가능성 추가 완화) Co-Authored-By: Claude Sonnet 4.6 --- .../consumer/ProblemAnalysisConsumer.java | 2 +- .../service/ProblemAnalysisService.java | 80 ++++++++++--------- 2 files changed, 44 insertions(+), 38 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/config/rabbitmq/consumer/ProblemAnalysisConsumer.java b/src/main/java/com/aisip/OnO/backend/config/rabbitmq/consumer/ProblemAnalysisConsumer.java index 969b120e..68a8436c 100644 --- a/src/main/java/com/aisip/OnO/backend/config/rabbitmq/consumer/ProblemAnalysisConsumer.java +++ b/src/main/java/com/aisip/OnO/backend/config/rabbitmq/consumer/ProblemAnalysisConsumer.java @@ -30,7 +30,7 @@ public class ProblemAnalysisConsumer { * - concurrency: 동시 처리 개수 (1-3개, OpenAI Rate Limit 고려) * - 예외 발생 시 자동으로 재시도 → 실패하면 DLQ로 이동 */ - @RabbitListener(queues = RabbitMQConfig.GPT_ANALYSIS_QUEUE, concurrency = "1-3") + @RabbitListener(queues = RabbitMQConfig.GPT_ANALYSIS_QUEUE, concurrency = "1-2") public void handleAnalysisMessage(ProblemAnalysisMessage message) { log.info("RabbitMQ message received - queue: {}, operation: {}, problemId: {}, messageRetryCount: {}", RabbitMQConfig.GPT_ANALYSIS_QUEUE, "problem_analysis", message.getProblemId(), message.getRetryCount()); diff --git a/src/main/java/com/aisip/OnO/backend/problem/service/ProblemAnalysisService.java b/src/main/java/com/aisip/OnO/backend/problem/service/ProblemAnalysisService.java index 4f91c1fb..ed923d91 100644 --- a/src/main/java/com/aisip/OnO/backend/problem/service/ProblemAnalysisService.java +++ b/src/main/java/com/aisip/OnO/backend/problem/service/ProblemAnalysisService.java @@ -127,38 +127,23 @@ public void updateToRateLimitExceeded(Long problemId) { /** * 동기적으로 문제 이미지를 분석합니다 (RabbitMQ Consumer에서 호출) - * - 동시성 문제 해결: 이미 생성된 PROCESSING 엔티티만 조회하여 업데이트 + * - 트랜잭션을 OpenAI 호출 전후로 분리해 DB 커넥션 장기 점유를 방지 */ - @Transactional public void analyzeProblemSync(Long problemId) { log.info("Starting sync analysis for problemId: {}", problemId); try { - // 1. Problem 조회 - Problem problem = problemRepository.findById(problemId) - .orElseThrow(() -> new ApplicationException(ProblemErrorCase.PROBLEM_NOT_FOUND)); - - // 2. 문제 이미지 url 조회 - List problemImageUrls = problemImageDataRepository.findAllByProblemId(problemId) - .stream() - .filter(data -> data.getProblemImageType().equals(ProblemImageType.PROBLEM_IMAGE)) - .map(ProblemImageData::getImageUrl) - .toList(); - - // 3. 기존 분석 조회 (동시성 문제 해결: 새로 생성하지 않고 조회만) - ProblemAnalysis analysis = analysisRepository.findByProblemId(problemId) - .orElseThrow(() -> new ApplicationException(ProblemErrorCase.PROBLEM_ANALYSIS_NOT_FOUND)); - - // 4. 이미 완료된 경우 스킵 - if (analysis.getStatus().equals(AnalysisStatus.COMPLETED)) { + // Phase 1: 짧은 readOnly 트랜잭션으로 필요한 데이터 조회 + AnalysisPreparation prep = fetchAnalysisPreparation(problemId); + if (prep.alreadyCompleted()) { log.info("Analysis already completed for problemId: {}", problemId); return; } - // 5. OpenAI API 호출 (여러 이미지를 하나의 문제로 분석) - ProblemAnalysisResult result = openAIClient.analyzeImages(problemImageUrls); + // Phase 2: 트랜잭션 없이 OpenAI 호출 (DB 커넥션 미점유) + ProblemAnalysisResult result = openAIClient.analyzeImages(prep.imageUrls()); - // 6. keyPoints를 JSON 문자열로 변환 + // Phase 3: 결과를 짧은 쓰기 트랜잭션으로 저장 String keyPointsJson; try { keyPointsJson = objectMapper.writeValueAsString(result.getKeyPoints()); @@ -166,27 +151,14 @@ public void analyzeProblemSync(Long problemId) { log.error("JSON 변환 실패 for problemId: {}", problemId, jsonException); throw new RuntimeException("JSON 변환 실패", jsonException); } - - // 7. 분석 결과 업데이트 (COMPLETED) - analysis.updateWithSuccess( - result.getSubject(), - result.getProblemType(), - keyPointsJson, - result.getSolution(), - result.getCommonMistakes(), - result.getStudyTips() - ); - - analysisRepository.save(analysis); + saveAnalysisSuccess(problemId, result, keyPointsJson); log.info("Analysis completed successfully for problemId: {}", problemId); } catch (ApplicationException e) { - // 삭제된 문제는 재시도 가치가 없어 상위 Consumer에서 비재시도 처리한다. if (e.getErrorCase() == ProblemErrorCase.PROBLEM_NOT_FOUND) { log.warn("Skipping analysis because problem was deleted or missing. problemId: {}", problemId); throw e; } - log.error("Application error during sync analysis for problemId: {}", problemId, e); handleAnalysisError(problemId, e); throw e; @@ -197,10 +169,44 @@ public void analyzeProblemSync(Long problemId) { } catch (Exception e) { log.error("Error during sync analysis for problemId: {}", problemId, e); handleAnalysisError(problemId, e); - throw e; // RabbitMQ 재시도를 위해 예외 다시 던짐 + throw e; } } + @Transactional(readOnly = true) + public AnalysisPreparation fetchAnalysisPreparation(Long problemId) { + problemRepository.findById(problemId) + .orElseThrow(() -> new ApplicationException(ProblemErrorCase.PROBLEM_NOT_FOUND)); + + List imageUrls = problemImageDataRepository.findAllByProblemId(problemId) + .stream() + .filter(data -> data.getProblemImageType().equals(ProblemImageType.PROBLEM_IMAGE)) + .map(ProblemImageData::getImageUrl) + .toList(); + + ProblemAnalysis analysis = analysisRepository.findByProblemId(problemId) + .orElseThrow(() -> new ApplicationException(ProblemErrorCase.PROBLEM_ANALYSIS_NOT_FOUND)); + + return new AnalysisPreparation(imageUrls, analysis.getStatus().equals(AnalysisStatus.COMPLETED)); + } + + @Transactional + public void saveAnalysisSuccess(Long problemId, ProblemAnalysisResult result, String keyPointsJson) { + ProblemAnalysis analysis = analysisRepository.findByProblemId(problemId) + .orElseThrow(() -> new ApplicationException(ProblemErrorCase.PROBLEM_ANALYSIS_NOT_FOUND)); + analysis.updateWithSuccess( + result.getSubject(), + result.getProblemType(), + keyPointsJson, + result.getSolution(), + result.getCommonMistakes(), + result.getStudyTips() + ); + analysisRepository.save(analysis); + } + + public record AnalysisPreparation(List imageUrls, boolean alreadyCompleted) {} + /** * 비동기로 문제 이미지를 분석합니다 (기존 @Async 방식, deprecated) * @deprecated RabbitMQ 방식(analyzeProblemSync)을 사용하세요 From 85a9bcfa9c5a0c78d6b39544becf7ea385ed6e57 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 30 Jun 2026 00:16:03 +0900 Subject: [PATCH 51/67] =?UTF-8?q?[Feat]=20=EB=9E=9C=EB=94=A9=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=8A=A4=ED=81=AC=EB=A6=B0=EC=83=B7=20?= =?UTF-8?q?=EA=B0=84=EA=B2=A9=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=EB=94=94=EB=A3=B8=20=EC=84=B9=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - phone-duo gap 12px → 24px (노트북 기준 스크린샷 간격 개선) - 스터디룸 피처 섹션 추가: 스터디룸 개설 · 챌린지 · 학습 피드 · 주간 리포트 설명 Co-Authored-By: Claude Sonnet 4.6 --- src/main/resources/templates/home.html | 85 +++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index d2fbc7be..cdb16be0 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -283,7 +283,7 @@ display: flex; align-items: flex-end; justify-content: center; - gap: 12px; + gap: 24px; } .phone-duo .phone-img { max-width: 188px; } .phone-duo .phone-img:last-child { @@ -367,6 +367,41 @@ font-size: .83rem; color: var(--t3); } + /* ── STUDY ROOM CARDS ── */ + .study-cards { + display: flex; + flex-direction: column; + gap: 16px; + max-width: 380px; + } + .study-card { + display: flex; + align-items: flex-start; + gap: 16px; + background: var(--bg2); + border-radius: 18px; + padding: 20px 22px; + } + .study-card-icon { + font-size: 1.75rem; + flex-shrink: 0; + line-height: 1; + } + .study-card strong { + display: block; + font-size: .94rem; + font-weight: 700; + color: var(--t1); + margin-bottom: 4px; + } + .study-card p { + font-size: .84rem; + color: var(--t2); + line-height: 1.55; + word-break: keep-all; + margin: 0; + } + /* ── RESPONSIVE ── */ @media (max-width: 860px) { .feat-inner { @@ -378,7 +413,9 @@ .feat-copy .band-p { max-width: none; } .feat-img-wrap .phone-img { max-width: 240px; } .phone-duo .phone-img { max-width: 155px; } + .phone-duo { gap: 16px; } .stat-row { justify-content: center; } + .study-cards { max-width: none; } .achieve-inner { grid-template-columns: 1fr; @@ -625,6 +662,52 @@

꾸준함이
실력이 됩니다

+ +
+
+
+ 스터디룸 +

함께 공부하면
더 잘 됩니다

+

+ 친구들과 스터디룸을 만들고, 챌린지와 피드로 서로의 학습을 응원하세요. + 혼자보다 훨씬 멀리 갈 수 있습니다. +

+
+
+
+
+ 👥 +
+ 스터디룸 개설 +

초대 코드로 친구를 불러 함께 공부하는 공간을 만드세요.

+
+
+
+ 🏆 +
+ 챌린지 설정 +

문제 등록 수, 복습 횟수 등 목표를 정하고 함께 달성해요.

+
+
+
+ 📰 +
+ 학습 피드 +

멤버들의 오답노트·복습 활동을 실시간으로 확인하고 응원해요.

+
+
+
+ 📊 +
+ 주간 리포트 +

팀원들의 주간 학습 성과를 한눈에 비교할 수 있어요.

+
+
+
+
+
+
+
From 55e466261837dd2275250b61488165335993f039 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 30 Jun 2026 00:17:00 +0900 Subject: [PATCH 52/67] =?UTF-8?q?[Perf]=20=EC=98=A4=EB=8B=B5=EB=85=B8?= =?UTF-8?q?=ED=8A=B8=20=EC=97=AC=EB=9F=AC=20=EC=9E=A5=20=EB=93=B1=EB=A1=9D?= =?UTF-8?q?=20Hibernate=20JDBC=20=EB=B0=B0=EC=B9=98=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HibernatePropertiesCustomizer로 batch_size=50, order_inserts/updates=true 적용 - saveAll 호출 시 INSERT/UPDATE 묶음 처리로 DB 라운드트립 감소 Co-Authored-By: Claude Sonnet 4.6 --- .../backend/common/config/JpaBatchConfig.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/java/com/aisip/OnO/backend/common/config/JpaBatchConfig.java diff --git a/src/main/java/com/aisip/OnO/backend/common/config/JpaBatchConfig.java b/src/main/java/com/aisip/OnO/backend/common/config/JpaBatchConfig.java new file mode 100644 index 00000000..382100ab --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/common/config/JpaBatchConfig.java @@ -0,0 +1,21 @@ +package com.aisip.OnO.backend.common.config; + +import org.hibernate.cfg.AvailableSettings; +import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Map; + +@Configuration +public class JpaBatchConfig { + + @Bean + public HibernatePropertiesCustomizer hibernateBatchCustomizer() { + return hibernateProperties -> hibernateProperties.putAll(Map.of( + AvailableSettings.STATEMENT_BATCH_SIZE, "50", + AvailableSettings.ORDER_INSERTS, "true", + AvailableSettings.ORDER_UPDATES, "true" + )); + } +} From 853ef8cc8594c357f7f917040e819f078a4e519e Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 30 Jun 2026 00:19:42 +0900 Subject: [PATCH 53/67] =?UTF-8?q?[Feat]=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EB=A3=B8=20=EC=B1=8C=EB=A6=B0=EC=A7=80=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?=EC=8B=9C=20=EC=A6=89=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - StudyRoomChallengeService.checkAndNotifyChallengeCompletionForUser 추가 (사용자가 속한 룸의 IN_PROGRESS 챌린지를 일괄 조회 후 완료 여부 즉시 확인) - StudyRoomFeedService.handleActivity에서 활동 발생 시 챌린지 완료 체크 호출 (문제 등록·복습 완료·문제 공유 시점에 즉시 알림 발송 가능) - REQUIRES_NEW 독립 트랜잭션으로 피드 생성 트랜잭션과 격리 Co-Authored-By: Claude Sonnet 4.6 --- .../service/StudyRoomChallengeService.java | 30 +++++++++++++++++++ .../service/StudyRoomFeedService.java | 6 ++++ 2 files changed, 36 insertions(+) diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java index 75cd7c00..32c660af 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java @@ -18,9 +18,11 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import org.springframework.transaction.annotation.Propagation; @Slf4j @Service @@ -225,6 +227,34 @@ private void validateCreateRequest(ChallengeCreateRequest request) { } } + /** + * 사용자의 학습 활동 이후 해당 사용자가 속한 룸들의 IN_PROGRESS 챌린지 완료 여부를 즉시 확인합니다. + * REQUIRES_NEW로 독립 트랜잭션 실행 — 실패해도 피드 생성 트랜잭션에 영향 없음. + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void checkAndNotifyChallengeCompletionForUser(Long userId) { + List memberships = memberRepository.findAllWithRoomByUserId(userId); + if (memberships.isEmpty()) { + return; + } + + List roomIds = memberships.stream() + .map(m -> m.getRoom().getId()) + .toList(); + List inProgressChallenges = + challengeRepository.findAllByRoomIdsAndStatus(roomIds, StudyRoomChallengeStatus.IN_PROGRESS); + if (inProgressChallenges.isEmpty()) { + return; + } + + Map> membersByRoom = new HashMap<>(); + for (StudyRoomChallenge challenge : inProgressChallenges) { + Long roomId = challenge.getRoom().getId(); + membersByRoom.computeIfAbsent(roomId, id -> memberRepository.findAllWithUserByRoomId(id)); + toResponse(challenge, membersByRoom.get(roomId), true); + } + } + private void notifyChallengeCompleted(StudyRoomChallenge challenge, List members) { NotificationRequestDto dto = new NotificationRequestDto(null, "챌린지 달성! 🎉", diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedService.java index 5f1642f7..51921216 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomFeedService.java @@ -47,6 +47,7 @@ public class StudyRoomFeedService { private final ObjectMapper objectMapper; private final StudyRoomReactionService reactionService; private final CustomEmojiValidator customEmojiValidator; + private final StudyRoomChallengeService challengeService; @Transactional(readOnly = true) public CursorPageResponse getFeed(Long roomId, Long userId, Long cursor, int size) { @@ -89,6 +90,11 @@ public void handleActivity(StudyRoomActivityEvent event) { } catch (Exception e) { log.error("Failed to create study room feed. userId: {}, eventType: {}", event.userId(), event.eventType(), e); } + try { + challengeService.checkAndNotifyChallengeCompletionForUser(event.userId()); + } catch (Exception e) { + log.error("Failed to check challenge completion. userId: {}, eventType: {}", event.userId(), event.eventType(), e); + } } @Transactional From 6fd81ac708b933e6028290e5dad0f90ddd2ec997 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 30 Jun 2026 00:32:17 +0900 Subject: [PATCH 54/67] =?UTF-8?q?[Fix]=20ProblemAnalysisService=20self-inv?= =?UTF-8?q?ocation=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ProblemAnalysisDbService 신규 빈으로 fetchAnalysisPreparation, saveAnalysisSuccess 분리 → analyzeProblemSync에서 직접 self-call 대신 별도 빈 호출로 AOP 프록시 정상 적용 - analyzeProblemSync에 @Transactional(NOT_SUPPORTED) 추가 → OpenAI 호출 중 DB 커넥션 미점유 설계 의도 실제 동작 - saveAnalysisSuccess에 COMPLETED 상태 중복 체크 추가 → 동시 처리 시 이미 완료된 분석 덮어쓰기 방지 - PROBLEM_ANALYSIS_NOT_FOUND 예외를 Consumer와 Service 양쪽에서 graceful skip 처리 → 문제 삭제 후 Phase 3 실행 시 불필요한 DLQ 처리 방지 Co-Authored-By: Claude Sonnet 4.6 --- .../consumer/ProblemAnalysisConsumer.java | 3 +- .../service/ProblemAnalysisDbService.java | 71 +++++++++++++++++++ .../service/ProblemAnalysisService.java | 49 +++---------- 3 files changed, 83 insertions(+), 40 deletions(-) create mode 100644 src/main/java/com/aisip/OnO/backend/problem/service/ProblemAnalysisDbService.java diff --git a/src/main/java/com/aisip/OnO/backend/config/rabbitmq/consumer/ProblemAnalysisConsumer.java b/src/main/java/com/aisip/OnO/backend/config/rabbitmq/consumer/ProblemAnalysisConsumer.java index 68a8436c..5952a60c 100644 --- a/src/main/java/com/aisip/OnO/backend/config/rabbitmq/consumer/ProblemAnalysisConsumer.java +++ b/src/main/java/com/aisip/OnO/backend/config/rabbitmq/consumer/ProblemAnalysisConsumer.java @@ -49,7 +49,8 @@ public void handleAnalysisMessage(ProblemAnalysisMessage message) { // 상태는 Service에서 FAILED로 업데이트됨. 재큐잉 없이 종료(ACK) return; } catch (ApplicationException e) { - if (e.getErrorCase() == ProblemErrorCase.PROBLEM_NOT_FOUND) { + if (e.getErrorCase() == ProblemErrorCase.PROBLEM_NOT_FOUND + || e.getErrorCase() == ProblemErrorCase.PROBLEM_ANALYSIS_NOT_FOUND) { log.warn("RabbitMQ message skipped - queue: {}, operation: {}, outcome: {}, problemId: {}, messageRetryCount: {}", RabbitMQConfig.GPT_ANALYSIS_QUEUE, "problem_analysis", "resource_not_found", message.getProblemId(), message.getRetryCount()); diff --git a/src/main/java/com/aisip/OnO/backend/problem/service/ProblemAnalysisDbService.java b/src/main/java/com/aisip/OnO/backend/problem/service/ProblemAnalysisDbService.java new file mode 100644 index 00000000..21c7ed5a --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/problem/service/ProblemAnalysisDbService.java @@ -0,0 +1,71 @@ +package com.aisip.OnO.backend.problem.service; + +import com.aisip.OnO.backend.common.exception.ApplicationException; +import com.aisip.OnO.backend.problem.entity.AnalysisStatus; +import com.aisip.OnO.backend.problem.entity.ProblemAnalysis; +import com.aisip.OnO.backend.problem.entity.ProblemImageData; +import com.aisip.OnO.backend.problem.entity.ProblemImageType; +import com.aisip.OnO.backend.problem.exception.ProblemErrorCase; +import com.aisip.OnO.backend.problem.repository.ProblemAnalysisRepository; +import com.aisip.OnO.backend.problem.repository.ProblemImageDataRepository; +import com.aisip.OnO.backend.problem.repository.ProblemRepository; +import com.aisip.OnO.backend.util.ai.ProblemAnalysisResult; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * ProblemAnalysisService의 DB 접근 작업을 분리한 별도 빈. + * analyzeProblemSync에서 self-invocation 없이 AOP 프록시를 거쳐 호출되어야 @Transactional이 정상 동작함. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ProblemAnalysisDbService { + + private final ProblemRepository problemRepository; + private final ProblemImageDataRepository problemImageDataRepository; + private final ProblemAnalysisRepository analysisRepository; + + @Transactional(readOnly = true) + public AnalysisPreparation fetchAnalysisPreparation(Long problemId) { + problemRepository.findById(problemId) + .orElseThrow(() -> new ApplicationException(ProblemErrorCase.PROBLEM_NOT_FOUND)); + + List imageUrls = problemImageDataRepository.findAllByProblemId(problemId) + .stream() + .filter(data -> data.getProblemImageType().equals(ProblemImageType.PROBLEM_IMAGE)) + .map(ProblemImageData::getImageUrl) + .toList(); + + ProblemAnalysis analysis = analysisRepository.findByProblemId(problemId) + .orElseThrow(() -> new ApplicationException(ProblemErrorCase.PROBLEM_ANALYSIS_NOT_FOUND)); + + return new AnalysisPreparation(imageUrls, analysis.getStatus().equals(AnalysisStatus.COMPLETED)); + } + + @Transactional + public void saveAnalysisSuccess(Long problemId, ProblemAnalysisResult result, String keyPointsJson) { + ProblemAnalysis analysis = analysisRepository.findByProblemId(problemId) + .orElseThrow(() -> new ApplicationException(ProblemErrorCase.PROBLEM_ANALYSIS_NOT_FOUND)); + // 다른 스레드가 먼저 완료한 경우 덮어쓰기 방지 + if (analysis.getStatus().equals(AnalysisStatus.COMPLETED)) { + log.info("Analysis already COMPLETED by another thread, skipping save. problemId: {}", problemId); + return; + } + analysis.updateWithSuccess( + result.getSubject(), + result.getProblemType(), + keyPointsJson, + result.getSolution(), + result.getCommonMistakes(), + result.getStudyTips() + ); + analysisRepository.save(analysis); + } + + public record AnalysisPreparation(List imageUrls, boolean alreadyCompleted) {} +} diff --git a/src/main/java/com/aisip/OnO/backend/problem/service/ProblemAnalysisService.java b/src/main/java/com/aisip/OnO/backend/problem/service/ProblemAnalysisService.java index ed923d91..53e2a709 100644 --- a/src/main/java/com/aisip/OnO/backend/problem/service/ProblemAnalysisService.java +++ b/src/main/java/com/aisip/OnO/backend/problem/service/ProblemAnalysisService.java @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -35,6 +36,7 @@ public class ProblemAnalysisService { private final ObjectMapper objectMapper; private final ProblemImageDataRepository problemImageDataRepository; private final ProblemAnalysisFailureService problemAnalysisFailureService; + private final ProblemAnalysisDbService problemAnalysisDbService; /** * 기존 분석 결과 삭제 @@ -127,14 +129,16 @@ public void updateToRateLimitExceeded(Long problemId) { /** * 동기적으로 문제 이미지를 분석합니다 (RabbitMQ Consumer에서 호출) - * - 트랜잭션을 OpenAI 호출 전후로 분리해 DB 커넥션 장기 점유를 방지 + * - NOT_SUPPORTED: 이 메서드 자체는 트랜잭션 없이 실행, DB 작업은 problemAnalysisDbService 빈을 통해 각자 독립 트랜잭션으로 처리 + * - self-invocation 문제를 피하기 위해 Phase 1/3의 DB 작업을 별도 빈에서 처리 */ + @Transactional(propagation = Propagation.NOT_SUPPORTED) public void analyzeProblemSync(Long problemId) { log.info("Starting sync analysis for problemId: {}", problemId); try { // Phase 1: 짧은 readOnly 트랜잭션으로 필요한 데이터 조회 - AnalysisPreparation prep = fetchAnalysisPreparation(problemId); + ProblemAnalysisDbService.AnalysisPreparation prep = problemAnalysisDbService.fetchAnalysisPreparation(problemId); if (prep.alreadyCompleted()) { log.info("Analysis already completed for problemId: {}", problemId); return; @@ -151,12 +155,13 @@ public void analyzeProblemSync(Long problemId) { log.error("JSON 변환 실패 for problemId: {}", problemId, jsonException); throw new RuntimeException("JSON 변환 실패", jsonException); } - saveAnalysisSuccess(problemId, result, keyPointsJson); + problemAnalysisDbService.saveAnalysisSuccess(problemId, result, keyPointsJson); log.info("Analysis completed successfully for problemId: {}", problemId); } catch (ApplicationException e) { - if (e.getErrorCase() == ProblemErrorCase.PROBLEM_NOT_FOUND) { - log.warn("Skipping analysis because problem was deleted or missing. problemId: {}", problemId); + if (e.getErrorCase() == ProblemErrorCase.PROBLEM_NOT_FOUND + || e.getErrorCase() == ProblemErrorCase.PROBLEM_ANALYSIS_NOT_FOUND) { + log.warn("Skipping analysis because problem or analysis was deleted. problemId: {}", problemId); throw e; } log.error("Application error during sync analysis for problemId: {}", problemId, e); @@ -173,40 +178,6 @@ public void analyzeProblemSync(Long problemId) { } } - @Transactional(readOnly = true) - public AnalysisPreparation fetchAnalysisPreparation(Long problemId) { - problemRepository.findById(problemId) - .orElseThrow(() -> new ApplicationException(ProblemErrorCase.PROBLEM_NOT_FOUND)); - - List imageUrls = problemImageDataRepository.findAllByProblemId(problemId) - .stream() - .filter(data -> data.getProblemImageType().equals(ProblemImageType.PROBLEM_IMAGE)) - .map(ProblemImageData::getImageUrl) - .toList(); - - ProblemAnalysis analysis = analysisRepository.findByProblemId(problemId) - .orElseThrow(() -> new ApplicationException(ProblemErrorCase.PROBLEM_ANALYSIS_NOT_FOUND)); - - return new AnalysisPreparation(imageUrls, analysis.getStatus().equals(AnalysisStatus.COMPLETED)); - } - - @Transactional - public void saveAnalysisSuccess(Long problemId, ProblemAnalysisResult result, String keyPointsJson) { - ProblemAnalysis analysis = analysisRepository.findByProblemId(problemId) - .orElseThrow(() -> new ApplicationException(ProblemErrorCase.PROBLEM_ANALYSIS_NOT_FOUND)); - analysis.updateWithSuccess( - result.getSubject(), - result.getProblemType(), - keyPointsJson, - result.getSolution(), - result.getCommonMistakes(), - result.getStudyTips() - ); - analysisRepository.save(analysis); - } - - public record AnalysisPreparation(List imageUrls, boolean alreadyCompleted) {} - /** * 비동기로 문제 이미지를 분석합니다 (기존 @Async 방식, deprecated) * @deprecated RabbitMQ 방식(analyzeProblemSync)을 사용하세요 From fe24124b08fe7a2d4dda1ff93f13ecbff566b48c Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 30 Jun 2026 00:33:58 +0900 Subject: [PATCH 55/67] =?UTF-8?q?[Fix]=20=EC=B1=8C=EB=A6=B0=EC=A7=80=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20FCM=20=EC=A4=91=EB=B3=B5=20=EB=B0=9C?= =?UTF-8?q?=EC=86=A1=20=EB=A0=88=EC=9D=B4=EC=8A=A4=20=EC=BB=A8=EB=94=94?= =?UTF-8?q?=EC=85=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - V15 마이그레이션: study_room_challenge 테이블에 completed_at 컬럼 추가 - StudyRoomChallengeRepository에 tryTransitionFromInProgress 추가 → UPDATE WHERE status = 'IN_PROGRESS' 원자적 실행으로 단 하나의 스레드만 1 반환 - StudyRoomChallengeService.toResponse에서 COMPLETED 전이 시 원자적 쿼리 사용 → 동시 트랜잭션이 같은 챌린지를 동시에 완료 처리해도 FCM은 1회만 발송 Co-Authored-By: Claude Sonnet 4.6 --- .../backend/studyroom/entity/StudyRoomChallenge.java | 3 +++ .../repository/StudyRoomChallengeRepository.java | 12 ++++++++++++ .../studyroom/service/StudyRoomChallengeService.java | 10 ++++++++-- .../db/migration/V15__add_challenge_completed_at.sql | 2 ++ 4 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 src/main/resources/db/migration/V15__add_challenge_completed_at.sql diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallenge.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallenge.java index 220d245c..2a35e04a 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallenge.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomChallenge.java @@ -55,6 +55,9 @@ public class StudyRoomChallenge extends BaseEntity { @Column(nullable = false, length = 20) private StudyRoomChallengeStatus status; + @Column(name = "completed_at") + private LocalDateTime completedAt; + public static StudyRoomChallenge create(StudyRoom room, String title, StudyRoomChallengeType type, StudyRoomChallengeMetric metric, StudyRoomChallengePeriod period, Integer periodDays, Integer targetValue, diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomChallengeRepository.java b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomChallengeRepository.java index f7aa4d84..775948e3 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomChallengeRepository.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/repository/StudyRoomChallengeRepository.java @@ -3,6 +3,7 @@ import com.aisip.OnO.backend.studyroom.entity.StudyRoomChallenge; import com.aisip.OnO.backend.studyroom.entity.StudyRoomChallengeStatus; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -38,4 +39,15 @@ List countByRoomIdsAndStatusAndEndAtBetween(@Param("roomIds") Collecti @Param("status") StudyRoomChallengeStatus status, @Param("start") LocalDateTime start, @Param("end") LocalDateTime end); + + /** + * IN_PROGRESS 상태인 챌린지를 원자적으로 targetStatus로 전이. + * 동시 호출 시 단 하나의 스레드만 1을 반환 — FCM 중복 발송 방지에 사용. + */ + @Modifying + @Query("update StudyRoomChallenge c set c.status = :targetStatus, c.completedAt = :now " + + "where c.id = :id and c.status = 'IN_PROGRESS'") + int tryTransitionFromInProgress(@Param("id") Long id, + @Param("targetStatus") StudyRoomChallengeStatus targetStatus, + @Param("now") LocalDateTime now); } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java index 32c660af..a8c2acc6 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java @@ -119,9 +119,15 @@ private ChallengeResponse toResponse(StudyRoomChallenge challenge, List 0) { + challenge.updateStatus(status); + notifyChallengeCompleted(challenge, members); + } + } else { + challenge.updateStatus(status); } } return new ChallengeResponse( diff --git a/src/main/resources/db/migration/V15__add_challenge_completed_at.sql b/src/main/resources/db/migration/V15__add_challenge_completed_at.sql new file mode 100644 index 00000000..72e8a4b1 --- /dev/null +++ b/src/main/resources/db/migration/V15__add_challenge_completed_at.sql @@ -0,0 +1,2 @@ +ALTER TABLE study_room_challenge + ADD COLUMN completed_at DATETIME NULL COMMENT '챌린지 완료 시각 (최초 완료 시 1회만 기록)'; From 391c9fa4b4a6002fd8c960c7f239eb0c0f8a9234 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 30 Jun 2026 10:26:54 +0900 Subject: [PATCH 56/67] =?UTF-8?q?[Feat]=20S3=20Presigned=20URL=20=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/fileUpload/presigned-urls?count=N&contentType=image/jpeg - 앱이 백엔드 경유 없이 S3에 직접 PUT 업로드 가능 - presignedUrl(10분 유효) + fileUrl(업로드 후 백엔드에 전달할 최종 URL) 반환 - count 최대 20 제한 - 기존 multipart 업로드 엔드포인트는 그대로 유지 (구 버전 앱 호환) Co-Authored-By: Claude Sonnet 4.6 --- .../controller/FileUploadController.java | 13 ++++++ .../fileupload/dto/PresignedUrlResponse.java | 6 +++ .../fileupload/service/FileUploadService.java | 41 ++++++++++++++++--- 3 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/aisip/OnO/backend/util/fileupload/dto/PresignedUrlResponse.java diff --git a/src/main/java/com/aisip/OnO/backend/util/fileupload/controller/FileUploadController.java b/src/main/java/com/aisip/OnO/backend/util/fileupload/controller/FileUploadController.java index 80cba693..2d7e5a81 100644 --- a/src/main/java/com/aisip/OnO/backend/util/fileupload/controller/FileUploadController.java +++ b/src/main/java/com/aisip/OnO/backend/util/fileupload/controller/FileUploadController.java @@ -1,6 +1,7 @@ package com.aisip.OnO.backend.util.fileupload.controller; import com.aisip.OnO.backend.common.response.CommonResponse; +import com.aisip.OnO.backend.util.fileupload.dto.PresignedUrlResponse; import com.aisip.OnO.backend.util.fileupload.service.FileUploadService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -38,6 +39,18 @@ public CommonResponse> uploadMultipleImageFiles( return CommonResponse.success(imageUrls); } + // 앱이 S3에 직접 업로드할 수 있는 Presigned URL 발급 + // count: 발급할 URL 수 (최대 20) + // contentType: 업로드할 이미지 MIME 타입 (예: image/jpeg) + @GetMapping("/presigned-urls") + public CommonResponse> getPresignedUrls( + @RequestParam(defaultValue = "1") int count, + @RequestParam(defaultValue = "image/jpeg") String contentType + ) { + int safeCount = Math.min(count, 20); + return CommonResponse.success(fileUploadService.generatePresignedUrls(contentType, safeCount)); + } + @DeleteMapping("/image") public CommonResponse deleteImageFile( @RequestParam("imageUrl") String imageUrl diff --git a/src/main/java/com/aisip/OnO/backend/util/fileupload/dto/PresignedUrlResponse.java b/src/main/java/com/aisip/OnO/backend/util/fileupload/dto/PresignedUrlResponse.java new file mode 100644 index 00000000..a2326fcd --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/util/fileupload/dto/PresignedUrlResponse.java @@ -0,0 +1,6 @@ +package com.aisip.OnO.backend.util.fileupload.dto; + +public record PresignedUrlResponse( + String presignedUrl, + String fileUrl +) {} diff --git a/src/main/java/com/aisip/OnO/backend/util/fileupload/service/FileUploadService.java b/src/main/java/com/aisip/OnO/backend/util/fileupload/service/FileUploadService.java index 8229f6f9..37871052 100644 --- a/src/main/java/com/aisip/OnO/backend/util/fileupload/service/FileUploadService.java +++ b/src/main/java/com/aisip/OnO/backend/util/fileupload/service/FileUploadService.java @@ -7,14 +7,20 @@ import com.aisip.OnO.backend.problemsolve.entity.ProblemSolveImageData; import com.aisip.OnO.backend.problemsolve.exception.ProblemSolveErrorCase; import com.aisip.OnO.backend.problemsolve.repository.ProblemSolveImageDataRepository; +import com.aisip.OnO.backend.util.fileupload.dto.PresignedUrlResponse; import com.aisip.OnO.backend.util.fileupload.exception.FileUploadErrorCase; +import com.amazonaws.HttpMethod; import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.DeleteObjectRequest; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; import com.amazonaws.services.s3.model.ObjectMetadata; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; import java.io.IOException; +import java.util.Date; +import java.util.List; import java.util.Objects; +import java.util.stream.IntStream; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -60,6 +66,22 @@ public String uploadFileToS3(MultipartFile file) { return fileUrl; } + public List generatePresignedUrls(String contentType, int count) { + Date expiration = new Date(System.currentTimeMillis() + 10 * 60 * 1000L); // 10분 + + return IntStream.range(0, count) + .mapToObj(i -> { + String fileName = createFileNameForPresigned(contentType); + GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucket, fileName) + .withMethod(HttpMethod.PUT) + .withContentType(contentType) + .withExpiration(expiration); + String presignedUrl = amazonS3Client.generatePresignedUrl(request).toString(); + return new PresignedUrlResponse(presignedUrl, getFileUrl(fileName)); + }) + .toList(); + } + public void deleteImageFileFromS3(String imageUrl) { Timer.Sample sample = Timer.start(meterRegistry); String splitStr = ".com/"; @@ -108,13 +130,22 @@ private void deleteProblemSolveImageData(String imageUrl, Long userId) { private String createFileName(MultipartFile file) { String originalFilename = file.getOriginalFilename(); - String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); // 확장자 추출 + String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); - String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); // 날짜 기반 폴더 - String randomFileName = UUID.randomUUID().toString(); // 랜덤 UUID + String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); + return "image/" + datePath + "/" + UUID.randomUUID() + extension; + } - // 예: image/YYYY/MM/DD/랜덤값.png - return "image/" + datePath + "/" + randomFileName + extension; + private String createFileNameForPresigned(String contentType) { + String extension = switch (contentType) { + case "image/png" -> ".png"; + case "image/gif" -> ".gif"; + case "image/webp" -> ".webp"; + case "image/heic", "image/heif" -> ".heic"; + default -> ".jpg"; // image/jpeg, image/jpg, application/octet-stream 등 + }; + String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); + return "image/" + datePath + "/" + UUID.randomUUID() + extension; } From 2a857c2aa24ec1daaa9ca05ab22ee2607a65c3a3 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 30 Jun 2026 10:55:09 +0900 Subject: [PATCH 57/67] =?UTF-8?q?[Feat]=20Presigned=20URL=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20URL=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20API=203=EC=A2=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PATCH /api/study-room/{roomId}/thumbnail-url OnO S3 URL 검증 후 썸네일 교체, 기존 이미지 비동기 삭제 - POST /api/problems/{problemId}/imageData/urls 기존 이미지 유지하며 새 이미지 URL 추가, AI 분석 재트리거 - POST /api/problem-solves/{problemSolveId}/image-urls 복습 기록에 이미지 URL 추가 (순서 이어받기) - FileUploadService.validateS3Url(): OnO 버킷 URL 검증 공통 헬퍼 추가 - 기존 multipart API 전부 유지 (구버전 앱 호환) Co-Authored-By: Claude Sonnet 4.6 --- .../problem/controller/ProblemController.java | 14 ++++++++++++++ .../dto/AddProblemImageUrlsRequest.java | 8 ++++++++ .../problem/service/ProblemService.java | 17 +++++++++++++++++ .../controller/ProblemSolveController.java | 13 +++++++++++++ .../service/ProblemSolveService.java | 18 ++++++++++++++++++ .../controller/StudyRoomController.java | 6 ++++++ .../backend/studyroom/dto/StudyRoomDtos.java | 3 +++ .../studyroom/service/StudyRoomService.java | 11 +++++++++++ .../fileupload/service/FileUploadService.java | 9 +++++++++ 9 files changed, 99 insertions(+) create mode 100644 src/main/java/com/aisip/OnO/backend/problem/dto/AddProblemImageUrlsRequest.java diff --git a/src/main/java/com/aisip/OnO/backend/problem/controller/ProblemController.java b/src/main/java/com/aisip/OnO/backend/problem/controller/ProblemController.java index 513f0345..e30819a8 100644 --- a/src/main/java/com/aisip/OnO/backend/problem/controller/ProblemController.java +++ b/src/main/java/com/aisip/OnO/backend/problem/controller/ProblemController.java @@ -2,6 +2,7 @@ import com.aisip.OnO.backend.common.response.CommonResponse; import com.aisip.OnO.backend.common.response.CursorPageResponse; +import com.aisip.OnO.backend.problem.dto.AddProblemImageUrlsRequest; import com.aisip.OnO.backend.problem.dto.ProblemAnalysisResponseDto; import com.aisip.OnO.backend.problem.dto.ReviewDueResponseDto; import com.aisip.OnO.backend.problem.dto.ProblemDeleteRequestDto; @@ -165,6 +166,19 @@ public CommonResponse uploadProblemImages( return CommonResponse.success("이미지 업로드가 시작되었습니다."); } + // ✅ 문제 이미지 URL 추가 (Presigned URL 방식) + @PostMapping("/{problemId}/imageData/urls") + public CommonResponse addProblemImageDataUrls( + @PathVariable("problemId") Long problemId, + @RequestBody AddProblemImageUrlsRequest request + ) { + Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + problemService.addImageDataUrls(problemId, userId, request); + problemService.analysisProblem(problemId, userId); + + return CommonResponse.success(null); + } + // ✅ 문제 이미지 비동기 업로드 @PatchMapping("/{problemId}/no-image") public CommonResponse updateProblemAnalysisStatus( diff --git a/src/main/java/com/aisip/OnO/backend/problem/dto/AddProblemImageUrlsRequest.java b/src/main/java/com/aisip/OnO/backend/problem/dto/AddProblemImageUrlsRequest.java new file mode 100644 index 00000000..9e52a953 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/problem/dto/AddProblemImageUrlsRequest.java @@ -0,0 +1,8 @@ +package com.aisip.OnO.backend.problem.dto; + +import java.util.List; + +public record AddProblemImageUrlsRequest(List imageDataList) { + + public record ImageUrlItem(String imageUrl, String problemImageType) {} +} diff --git a/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java b/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java index 6d1a3459..823a47e4 100644 --- a/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java +++ b/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java @@ -11,6 +11,7 @@ import com.aisip.OnO.backend.problem.entity.ProblemAnalysis; import com.aisip.OnO.backend.problem.entity.ProblemImageType; import com.aisip.OnO.backend.util.fileupload.service.FileUploadService; +import com.aisip.OnO.backend.problem.dto.AddProblemImageUrlsRequest; import com.aisip.OnO.backend.problem.dto.ProblemImageDataRegisterDto; import com.aisip.OnO.backend.problem.dto.ProblemRegisterDto; import com.aisip.OnO.backend.problem.dto.ProblemRegisterV2BatchDto; @@ -482,6 +483,22 @@ public void uploadProblemImages(Long problemId, Long userId, List } } + @Transactional + public void addImageDataUrls(Long problemId, Long userId, AddProblemImageUrlsRequest request) { + Problem problem = findProblemEntity(problemId, userId); + for (AddProblemImageUrlsRequest.ImageUrlItem item : request.imageDataList()) { + fileUploadService.validateS3Url(item.imageUrl()); + ProblemImageType imageType = ProblemImageType.valueOf(item.problemImageType()); + ProblemImageData imageData = ProblemImageData.from( + new ProblemImageDataRegisterDto(problemId, item.imageUrl(), imageType)); + imageData.updateProblem(problem); + problemImageDataRepository.save(imageData); + if (imageType == ProblemImageType.SOLVE_IMAGE) { + missionLogService.registerProblemPracticeMission(userId, problemId); + } + } + } + /** * GPT 문제 분석 요청 (RabbitMQ 방식) * - 이미지 유무 확인 후 상태 업데이트 diff --git a/src/main/java/com/aisip/OnO/backend/problemsolve/controller/ProblemSolveController.java b/src/main/java/com/aisip/OnO/backend/problemsolve/controller/ProblemSolveController.java index a4a4d361..4c36792b 100644 --- a/src/main/java/com/aisip/OnO/backend/problemsolve/controller/ProblemSolveController.java +++ b/src/main/java/com/aisip/OnO/backend/problemsolve/controller/ProblemSolveController.java @@ -88,6 +88,19 @@ public CommonResponse uploadProblemSolveImages( return CommonResponse.success("복습 기록 이미지 업로드가 완료되었습니다."); } + // 복습 기록 이미지 URL 추가 (Presigned URL 방식) + @PostMapping("/{problemSolveId}/image-urls") + public CommonResponse addProblemSolveImageUrls( + @PathVariable("problemSolveId") Long problemSolveId, + @RequestBody ImageUrlsRequest request) { + Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + problemSolveService.addImageUrls(problemSolveId, userId, request.imageUrls()); + + return CommonResponse.success(null); + } + + public record ImageUrlsRequest(List imageUrls) {} + // 복습 기록 수정 @PatchMapping("") public CommonResponse updateProblemSolve(@RequestBody ProblemSolveUpdateDto updateDto) { diff --git a/src/main/java/com/aisip/OnO/backend/problemsolve/service/ProblemSolveService.java b/src/main/java/com/aisip/OnO/backend/problemsolve/service/ProblemSolveService.java index fb0b053e..1519e386 100644 --- a/src/main/java/com/aisip/OnO/backend/problemsolve/service/ProblemSolveService.java +++ b/src/main/java/com/aisip/OnO/backend/problemsolve/service/ProblemSolveService.java @@ -180,6 +180,24 @@ public void uploadProblemSolveImages(Long problemSolveId, Long userId, List imageUrls) { + ProblemSolve problemSolve = problemSolveRepository.findById(problemSolveId) + .orElseThrow(() -> new ApplicationException(ProblemSolveErrorCase.PROBLEM_SOLVE_NOT_FOUND)); + if (!Objects.equals(problemSolve.getUserId(), userId)) { + throw new ApplicationException(ProblemSolveErrorCase.PROBLEM_SOLVE_USER_UNMATCHED); + } + int existingCount = problemSolve.getImages() == null ? 0 : problemSolve.getImages().size(); + List imageDataList = IntStream.range(0, imageUrls.size()) + .mapToObj(i -> { + fileUploadService.validateS3Url(imageUrls.get(i)); + return ProblemSolveImageData.create(imageUrls.get(i), existingCount + i); + }) + .collect(Collectors.toList()); + imageDataList.forEach(problemSolve::addImage); + problemSolveImageDataRepository.saveAll(imageDataList); + } + @Transactional public void updateProblemSolve(ProblemSolveUpdateDto dto, Long userId) { ProblemSolve problemSolve = problemSolveRepository.findById(dto.problemSolveId()) diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java index a115f5c3..2905bad3 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/controller/StudyRoomController.java @@ -99,6 +99,12 @@ public CommonResponse updateThumbnail(@PathVar return CommonResponse.success(studyRoomService.updateThumbnail(roomId, currentUserId(), thumbnail)); } + @PatchMapping(value = "/{roomId}/thumbnail-url", consumes = MediaType.APPLICATION_JSON_VALUE) + public CommonResponse updateThumbnailByUrl(@PathVariable("roomId") Long roomId, + @RequestBody ThumbnailUrlUpdateRequest request) { + return CommonResponse.success(studyRoomService.updateThumbnailByUrl(roomId, currentUserId(), request.thumbnailUrl())); + } + private Long currentUserId() { return (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java index c5b5d1ee..80f8d74d 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java @@ -113,4 +113,7 @@ public record WeeklyReportResponse(Long reportId, LocalDate weekStart, LocalDate public record WeeklyReportReadResponse(Long reportId, boolean isRead) { } + + public record ThumbnailUrlUpdateRequest(String thumbnailUrl) { + } } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java index 1e1c53cb..19bf9715 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java @@ -200,6 +200,17 @@ public StudyRoomThumbnailUpdateResponse updateThumbnail(Long roomId, Long userId return new StudyRoomThumbnailUpdateResponse(thumbnailUrl); } + @Transactional + public StudyRoomThumbnailUpdateResponse updateThumbnailByUrl(Long roomId, Long userId, String thumbnailUrl) { + accessService.validateHost(roomId, userId); + fileUploadService.validateS3Url(thumbnailUrl); + StudyRoom room = accessService.getRoomOrThrow(roomId); + String previousThumbnailUrl = room.getThumbnailUrl(); + room.updateThumbnailUrl(thumbnailUrl); + deleteThumbnailAsync(previousThumbnailUrl, roomId); + return new StudyRoomThumbnailUpdateResponse(thumbnailUrl); + } + StudyRoomDetailResponse buildDetail(StudyRoom room) { List members = memberRepository.findAllWithUserByRoomId(room.getId()); List userIds = members.stream().map(member -> member.getUser().getId()).toList(); diff --git a/src/main/java/com/aisip/OnO/backend/util/fileupload/service/FileUploadService.java b/src/main/java/com/aisip/OnO/backend/util/fileupload/service/FileUploadService.java index 37871052..e23a8b32 100644 --- a/src/main/java/com/aisip/OnO/backend/util/fileupload/service/FileUploadService.java +++ b/src/main/java/com/aisip/OnO/backend/util/fileupload/service/FileUploadService.java @@ -66,6 +66,15 @@ public String uploadFileToS3(MultipartFile file) { return fileUrl; } + public void validateS3Url(String url) { + if (url == null || url.isBlank()) { + throw new ApplicationException(FileUploadErrorCase.INVALID_IMAGE_FILE); + } + if (!url.startsWith("https://" + bucket + ".s3.")) { + throw new ApplicationException(FileUploadErrorCase.INVALID_IMAGE_FILE); + } + } + public List generatePresignedUrls(String contentType, int count) { Date expiration = new Date(System.currentTimeMillis() + 10 * 60 * 1000L); // 10분 From 333cad6dab2414c2d2f0336a74b0def298af97b9 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 30 Jun 2026 11:32:25 +0900 Subject: [PATCH 58/67] =?UTF-8?q?[Feat]=20=EC=B1=8C=EB=A6=B0=EC=A7=80?= =?UTF-8?q?=C2=B7=EC=A3=BC=EA=B0=84=20=EB=A6=AC=ED=8F=AC=ED=8A=B8=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20Presigned=20URL=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ChallengeMemberProgressResponse에 profileImageUrl 필드 추가 - WeeklyReportResponse에 topMemberProfileImageUrl, longestStreakProfileImageUrl 추가 V16 마이그레이션으로 study_room_weekly_report 테이블에 컬럼 추가 리포트 생성 시 User.getProfileImageUrl() 값 저장 - PATCH /api/users/me/profile-image-url 추가 Presigned URL로 S3에 직접 업로드 후 URL만 전달하는 방식 지원 기존 multipart API 유지 Co-Authored-By: Claude Sonnet 4.6 --- .../OnO/backend/studyroom/dto/StudyRoomDtos.java | 6 +++--- .../studyroom/entity/StudyRoomWeeklyReport.java | 14 ++++++++++++-- .../service/StudyRoomChallengeService.java | 2 +- .../service/StudyRoomWeeklyReportService.java | 6 ++++-- .../backend/user/controller/UserController.java | 8 ++++++++ .../OnO/backend/user/service/UserService.java | 10 ++++++++++ .../V16__add_weekly_report_profile_image_urls.sql | 3 +++ 7 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 src/main/resources/db/migration/V16__add_weekly_report_profile_image_urls.sql diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java index 80f8d74d..599ad111 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java @@ -82,7 +82,7 @@ public record ChallengeResponse(Long challengeId, String title, String type, Str Integer groupCurrent) { } - public record ChallengeMemberProgressResponse(Long userId, String name, int current, boolean cleared) { + public record ChallengeMemberProgressResponse(Long userId, String name, String profileImageUrl, int current, boolean cleared) { } public record SharedProblemResponse(Long sharedProblemId, Long sharedByUserId, String sharedByName, @@ -105,8 +105,8 @@ public record SharedProblemCommentResponse(Long commentId, String content, Long } public record WeeklyReportResponse(Long reportId, LocalDate weekStart, LocalDate weekEnd, - String topMemberName, Integer topMemberProblemCount, - String longestStreakName, Integer longestStreakDays, + String topMemberName, String topMemberProfileImageUrl, Integer topMemberProblemCount, + String longestStreakName, String longestStreakProfileImageUrl, Integer longestStreakDays, Integer totalProblems, Integer challengesCompleted, String cheerMessage, boolean isRead) { } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomWeeklyReport.java b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomWeeklyReport.java index 93949cce..ad00a44c 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomWeeklyReport.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/entity/StudyRoomWeeklyReport.java @@ -41,6 +41,12 @@ public class StudyRoomWeeklyReport extends BaseEntity { @Column(name = "longest_streak_days", nullable = false) private Integer longestStreakDays; + @Column(name = "top_member_profile_image_url", length = 512) + private String topMemberProfileImageUrl; + + @Column(name = "longest_streak_profile_image_url", length = 512) + private String longestStreakProfileImageUrl; + @Column(name = "total_problems", nullable = false) private Integer totalProblems; @@ -51,16 +57,20 @@ public class StudyRoomWeeklyReport extends BaseEntity { private String cheerMessage; public static StudyRoomWeeklyReport create(StudyRoom room, LocalDate weekStart, LocalDate weekEnd, - String topMemberName, int topMemberProblemCount, - String longestStreakName, int longestStreakDays, + String topMemberName, String topMemberProfileImageUrl, + int topMemberProblemCount, + String longestStreakName, String longestStreakProfileImageUrl, + int longestStreakDays, int totalProblems, int challengesCompleted, String cheerMessage) { return StudyRoomWeeklyReport.builder() .room(room) .weekStart(weekStart) .weekEnd(weekEnd) .topMemberName(topMemberName) + .topMemberProfileImageUrl(topMemberProfileImageUrl) .topMemberProblemCount(topMemberProblemCount) .longestStreakName(longestStreakName) + .longestStreakProfileImageUrl(longestStreakProfileImageUrl) .longestStreakDays(longestStreakDays) .totalProblems(totalProblems) .challengesCompleted(challengesCompleted) diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java index a8c2acc6..dbda23f2 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java @@ -108,7 +108,7 @@ private ChallengeResponse toResponse(StudyRoomChallenge challenge, List= challenge.getTargetValue()); + member.getUser().getProfileImageUrl(), current, current >= challenge.getTargetValue()); }) .toList(); Integer groupCurrent = challenge.getType() == StudyRoomChallengeType.GROUP diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomWeeklyReportService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomWeeklyReportService.java index 00f8c5cb..a8a915cc 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomWeeklyReportService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomWeeklyReportService.java @@ -134,8 +134,10 @@ private void createReportIfAbsent(StudyRoom room, List members, weekStart, weekEnd, topProblemMember == null ? null : topProblemMember.getUser().getName(), + topProblemMember == null ? null : topProblemMember.getUser().getProfileImageUrl(), topProblems, topStreakMember == null ? null : topStreakMember.getUser().getName(), + topStreakMember == null ? null : topStreakMember.getUser().getProfileImageUrl(), longestStreak, totalProblems, challengesCompleted, @@ -211,8 +213,8 @@ private LocalDate min(LocalDate left, LocalDate right) { private WeeklyReportResponse toResponse(StudyRoomWeeklyReport report, boolean read) { return new WeeklyReportResponse(report.getId(), report.getWeekStart(), report.getWeekEnd(), - report.getTopMemberName(), report.getTopMemberProblemCount(), - report.getLongestStreakName(), report.getLongestStreakDays(), + report.getTopMemberName(), report.getTopMemberProfileImageUrl(), report.getTopMemberProblemCount(), + report.getLongestStreakName(), report.getLongestStreakProfileImageUrl(), report.getLongestStreakDays(), report.getTotalProblems(), report.getChallengesCompleted(), report.getCheerMessage(), read); } diff --git a/src/main/java/com/aisip/OnO/backend/user/controller/UserController.java b/src/main/java/com/aisip/OnO/backend/user/controller/UserController.java index 2c80c1e6..422649c5 100644 --- a/src/main/java/com/aisip/OnO/backend/user/controller/UserController.java +++ b/src/main/java/com/aisip/OnO/backend/user/controller/UserController.java @@ -55,6 +55,12 @@ public CommonResponse updateProfileImage(@RequestParam("profile return CommonResponse.success(userService.updateProfileImage(userId, profileImage)); } + @PatchMapping(value = "/me/profile-image-url", consumes = MediaType.APPLICATION_JSON_VALUE) + public CommonResponse updateProfileImageByUrl(@RequestBody ProfileImageUrlRequest request) { + Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + return CommonResponse.success(userService.updateProfileImageByUrl(userId, request.profileImageUrl())); + } + @DeleteMapping("/me/profile-image") public CommonResponse deleteProfileImage() { Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); @@ -67,4 +73,6 @@ public void deleteUserInfo() { Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); userService.deleteUserById(userId); } + + public record ProfileImageUrlRequest(String profileImageUrl) {} } diff --git a/src/main/java/com/aisip/OnO/backend/user/service/UserService.java b/src/main/java/com/aisip/OnO/backend/user/service/UserService.java index e1aaad1d..4af288d3 100644 --- a/src/main/java/com/aisip/OnO/backend/user/service/UserService.java +++ b/src/main/java/com/aisip/OnO/backend/user/service/UserService.java @@ -171,6 +171,16 @@ public UserResponseDto updateProfileImage(Long userId, MultipartFile profileImag return UserResponseDto.from(user); } + @Transactional + public UserResponseDto updateProfileImageByUrl(Long userId, String imageUrl) { + fileUploadService.validateS3Url(imageUrl); + User user = findUserEntity(userId); + String previousProfileImageUrl = user.getProfileImageUrl(); + user.updateProfileImageUrl(imageUrl); + deleteImageAsync(previousProfileImageUrl, userId); + return UserResponseDto.from(user); + } + @Transactional public UserResponseDto deleteProfileImage(Long userId) { User user = findUserEntity(userId); diff --git a/src/main/resources/db/migration/V16__add_weekly_report_profile_image_urls.sql b/src/main/resources/db/migration/V16__add_weekly_report_profile_image_urls.sql new file mode 100644 index 00000000..3b4d2787 --- /dev/null +++ b/src/main/resources/db/migration/V16__add_weekly_report_profile_image_urls.sql @@ -0,0 +1,3 @@ +ALTER TABLE study_room_weekly_report + ADD COLUMN top_member_profile_image_url VARCHAR(512) NULL, + ADD COLUMN longest_streak_profile_image_url VARCHAR(512) NULL; From 9889c5ae1a7c2f1a7cd517191287bb68c42a54cf Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 30 Jun 2026 15:25:59 +0900 Subject: [PATCH 59/67] =?UTF-8?q?[Fix]=20getUserInfo=EC=97=90=EC=84=9C=20t?= =?UTF-8?q?ouchLastActiveAt=EA=B3=BC=20findUser=20=ED=8A=B8=EB=9E=9C?= =?UTF-8?q?=EC=9E=AD=EC=85=98=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UserService에 touchAndFindUser() 메서드 추가 (단일 트랜잭션) - UserController에서 두 번 호출하던 방식 제거 --- .../aisip/OnO/backend/user/controller/UserController.java | 4 +--- .../com/aisip/OnO/backend/user/service/UserService.java | 7 +++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/user/controller/UserController.java b/src/main/java/com/aisip/OnO/backend/user/controller/UserController.java index 422649c5..78e409fa 100644 --- a/src/main/java/com/aisip/OnO/backend/user/controller/UserController.java +++ b/src/main/java/com/aisip/OnO/backend/user/controller/UserController.java @@ -25,9 +25,7 @@ public class UserController { public CommonResponse getUserInfo() { Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); missionLogService.registerLoginMission(userId); - userService.touchLastActiveAt(userId); - - return CommonResponse.success(userService.findUser(userId)); + return CommonResponse.success(userService.touchAndFindUser(userId)); } // ✅ 사용자 정보 수정 diff --git a/src/main/java/com/aisip/OnO/backend/user/service/UserService.java b/src/main/java/com/aisip/OnO/backend/user/service/UserService.java index 4af288d3..e4c5e0c1 100644 --- a/src/main/java/com/aisip/OnO/backend/user/service/UserService.java +++ b/src/main/java/com/aisip/OnO/backend/user/service/UserService.java @@ -144,6 +144,13 @@ public void touchLastActiveAt(Long userId) { findUserEntity(userId).touchLastActiveAt(); } + @Transactional + public UserResponseDto touchAndFindUser(Long userId) { + User user = findUserEntity(userId); + user.touchLastActiveAt(); + return UserResponseDto.from(user); + } + @Transactional public void updateNotificationSettings(Long userId, boolean notificationEnabled) { findUserEntity(userId).updateNotificationEnabled(notificationEnabled); From 4d01708fa009ae5f73325b568def06b29b2fba4d Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 30 Jun 2026 15:26:05 +0900 Subject: [PATCH 60/67] =?UTF-8?q?[Feat]=20Discord=20Webhook=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=EC=9D=84=20RabbitMQ=20=EB=B9=84=EB=8F=99=EA=B8=B0=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DiscordWebhookQueue / DLQ / 바인딩 Bean 추가 (TTL 5분) - DiscordWebhookProducer, DiscordWebhookConsumer 구현 - DiscordWebhookNotificationService의 RestTemplate 직접 호출 제거 --- .../config/rabbitmq/RabbitMQConfig.java | 33 ++++++++++++ .../consumer/DiscordWebhookConsumer.java | 52 +++++++++++++++++++ .../message/DiscordWebhookMessage.java | 17 ++++++ .../producer/DiscordWebhookProducer.java | 30 +++++++++++ .../DiscordWebhookNotificationService.java | 45 +++++----------- 5 files changed, 145 insertions(+), 32 deletions(-) create mode 100644 src/main/java/com/aisip/OnO/backend/config/rabbitmq/consumer/DiscordWebhookConsumer.java create mode 100644 src/main/java/com/aisip/OnO/backend/config/rabbitmq/message/DiscordWebhookMessage.java create mode 100644 src/main/java/com/aisip/OnO/backend/config/rabbitmq/producer/DiscordWebhookProducer.java diff --git a/src/main/java/com/aisip/OnO/backend/config/rabbitmq/RabbitMQConfig.java b/src/main/java/com/aisip/OnO/backend/config/rabbitmq/RabbitMQConfig.java index 5bd61a59..97eb3ff0 100644 --- a/src/main/java/com/aisip/OnO/backend/config/rabbitmq/RabbitMQConfig.java +++ b/src/main/java/com/aisip/OnO/backend/config/rabbitmq/RabbitMQConfig.java @@ -34,10 +34,14 @@ public class RabbitMQConfig { public static final String FCM_NOTIFICATION_QUEUE = "fcm.notification.queue"; public static final String FCM_NOTIFICATION_DLQ = "fcm.notification.dlq"; + public static final String DISCORD_WEBHOOK_QUEUE = "discord.webhook.queue"; + public static final String DISCORD_WEBHOOK_DLQ = "discord.webhook.dlq"; + // ==================== Routing Key 정의 ==================== public static final String S3_DELETE_ROUTING_KEY = "s3.delete"; public static final String GPT_ANALYSIS_ROUTING_KEY = "gpt.analysis"; public static final String FCM_NOTIFICATION_ROUTING_KEY = "fcm.notification"; + public static final String DISCORD_WEBHOOK_ROUTING_KEY = "discord.webhook"; /** * JSON 메시지 컨버터 (객체를 JSON으로 직렬화/역직렬화) @@ -202,4 +206,33 @@ public Binding fcmNotificationBinding() { .to(notificationExchange()) .with(FCM_NOTIFICATION_ROUTING_KEY); } + + // ==================== Discord Webhook Queue ==================== + + /** + * Discord Webhook Queue + * - DLQ 설정 포함 + * - TTL: 5분 (300,000ms) + */ + @Bean + public Queue discordWebhookQueue() { + return QueueBuilder.durable(DISCORD_WEBHOOK_QUEUE) + .withArgument("x-dead-letter-exchange", "") + .withArgument("x-dead-letter-routing-key", DISCORD_WEBHOOK_DLQ) + .withArgument("x-message-ttl", 300000) + .build(); + } + + @Bean + public Queue discordWebhookDLQ() { + return QueueBuilder.durable(DISCORD_WEBHOOK_DLQ).build(); + } + + @Bean + public Binding discordWebhookBinding() { + return BindingBuilder + .bind(discordWebhookQueue()) + .to(notificationExchange()) + .with(DISCORD_WEBHOOK_ROUTING_KEY); + } } diff --git a/src/main/java/com/aisip/OnO/backend/config/rabbitmq/consumer/DiscordWebhookConsumer.java b/src/main/java/com/aisip/OnO/backend/config/rabbitmq/consumer/DiscordWebhookConsumer.java new file mode 100644 index 00000000..85e88dcb --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/config/rabbitmq/consumer/DiscordWebhookConsumer.java @@ -0,0 +1,52 @@ +package com.aisip.OnO.backend.config.rabbitmq.consumer; + +import com.aisip.OnO.backend.config.rabbitmq.RabbitMQConfig; +import com.aisip.OnO.backend.config.rabbitmq.message.DiscordWebhookMessage; +import com.aisip.OnO.backend.util.webhook.DiscordWebhookPayload; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Slf4j +@Service +public class DiscordWebhookConsumer { + + private final RestTemplate restTemplate; + + @Value("${discord.webhook-url}") + private String webhookUrl; + + public DiscordWebhookConsumer() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(3_000); + factory.setReadTimeout(5_000); + this.restTemplate = new RestTemplate(factory); + } + + @RabbitListener(queues = RabbitMQConfig.DISCORD_WEBHOOK_QUEUE, concurrency = "1-3") + public void handleWebhookMessage(DiscordWebhookMessage message) { + DiscordWebhookPayload payload = message.getPayload(); + try { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + restTemplate.postForEntity(webhookUrl, new HttpEntity<>(payload, headers), String.class); + log.info("Discord webhook 전송 완료: {}", payload.embeds().get(0).title()); + } catch (Exception e) { + log.error("Discord webhook 전송 실패: {}", e.getMessage(), e); + throw new RuntimeException("Discord webhook 전송 실패", e); + } + } + + @RabbitListener(queues = RabbitMQConfig.DISCORD_WEBHOOK_DLQ) + public void handleWebhookDLQ(DiscordWebhookMessage message) { + DiscordWebhookPayload payload = message.getPayload(); + String title = payload.embeds().isEmpty() ? "(제목 없음)" : payload.embeds().get(0).title(); + log.error("Discord webhook DLQ — 최종 전송 실패, 수동 확인 필요. title={}", title); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/config/rabbitmq/message/DiscordWebhookMessage.java b/src/main/java/com/aisip/OnO/backend/config/rabbitmq/message/DiscordWebhookMessage.java new file mode 100644 index 00000000..f3588ed4 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/config/rabbitmq/message/DiscordWebhookMessage.java @@ -0,0 +1,17 @@ +package com.aisip.OnO.backend.config.rabbitmq.message; + +import com.aisip.OnO.backend.util.webhook.DiscordWebhookPayload; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class DiscordWebhookMessage implements Serializable { + + private DiscordWebhookPayload payload; + private String dedupKey; +} diff --git a/src/main/java/com/aisip/OnO/backend/config/rabbitmq/producer/DiscordWebhookProducer.java b/src/main/java/com/aisip/OnO/backend/config/rabbitmq/producer/DiscordWebhookProducer.java new file mode 100644 index 00000000..1fa91018 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/config/rabbitmq/producer/DiscordWebhookProducer.java @@ -0,0 +1,30 @@ +package com.aisip.OnO.backend.config.rabbitmq.producer; + +import com.aisip.OnO.backend.config.rabbitmq.RabbitMQConfig; +import com.aisip.OnO.backend.config.rabbitmq.message.DiscordWebhookMessage; +import com.aisip.OnO.backend.util.webhook.DiscordWebhookPayload; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DiscordWebhookProducer { + + private final RabbitTemplate rabbitTemplate; + + public void send(DiscordWebhookPayload payload, String dedupKey) { + try { + rabbitTemplate.convertAndSend( + RabbitMQConfig.NOTIFICATION_EXCHANGE, + RabbitMQConfig.DISCORD_WEBHOOK_ROUTING_KEY, + new DiscordWebhookMessage(payload, dedupKey) + ); + log.debug("Discord webhook 메시지 큐 전송: {}", payload.embeds().get(0).title()); + } catch (Exception e) { + log.error("Discord webhook 큐 전송 실패: {}", e.getMessage(), e); + } + } +} diff --git a/src/main/java/com/aisip/OnO/backend/util/webhook/DiscordWebhookNotificationService.java b/src/main/java/com/aisip/OnO/backend/util/webhook/DiscordWebhookNotificationService.java index e74eb14a..9028f4d1 100644 --- a/src/main/java/com/aisip/OnO/backend/util/webhook/DiscordWebhookNotificationService.java +++ b/src/main/java/com/aisip/OnO/backend/util/webhook/DiscordWebhookNotificationService.java @@ -1,13 +1,9 @@ package com.aisip.OnO.backend.util.webhook; +import com.aisip.OnO.backend.config.rabbitmq.producer.DiscordWebhookProducer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; import java.time.Duration; import java.time.Instant; @@ -20,13 +16,10 @@ @RequiredArgsConstructor public class DiscordWebhookNotificationService { - private final RestTemplate restTemplate = new RestTemplate(); + private final DiscordWebhookProducer discordWebhookProducer; private final Map lastSentAtByDedupKey = new ConcurrentHashMap<>(); private static final Duration ERROR_NOTIFICATION_DEDUP_WINDOW = Duration.ofMinutes(5); - @Value("${discord.webhook-url}") - private String webhookUrl; - /** * 에러 발생 시 Discord로 알림 전송 */ @@ -51,7 +44,7 @@ public void sendErrorNotification(String path, String errorMessage, String statu ) ); - sendToDiscord(new DiscordWebhookPayload(null, List.of(embed)), dedupKey); + publishToQueue(new DiscordWebhookPayload(null, List.of(embed)), dedupKey); } /** @@ -67,7 +60,7 @@ public void sendMessage(String title, String message) { List.of() ); - sendToDiscord(new DiscordWebhookPayload(null, List.of(embed))); + publishToQueue(new DiscordWebhookPayload(null, List.of(embed))); } /** @@ -89,33 +82,21 @@ public void sendCustomEmbed(String title, String description, List(payload, headers), - String.class - ); - if (dedupKey != null) { - lastSentAtByDedupKey.put(dedupKey, Instant.now()); - } - log.info("Discord webhook 전송 완료: {}", payload.embeds().get(0).title()); - } catch (Exception e) { - log.error("Discord webhook 전송 실패: {}", e.getMessage(), e); - } + private void publishToQueue(DiscordWebhookPayload payload) { + publishToQueue(payload, null); } private boolean shouldSuppress(String dedupKey) { From c5f99683c58276026a1ce14bb6835ebdd6df0224 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 30 Jun 2026 15:26:10 +0900 Subject: [PATCH 61/67] =?UTF-8?q?[Fix]=20ProblemAnalysis=20DLQ=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=8B=9C=20PROCESSING=20=EA=B3=A0=EC=B0=A9=20?= =?UTF-8?q?=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DLQ 도달(최대 재시도 소진) 시 ProblemAnalysisFailureService.markFailed() 호출 - PROCESSING 상태 고착 방지 --- .../rabbitmq/consumer/ProblemAnalysisConsumer.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/com/aisip/OnO/backend/config/rabbitmq/consumer/ProblemAnalysisConsumer.java b/src/main/java/com/aisip/OnO/backend/config/rabbitmq/consumer/ProblemAnalysisConsumer.java index 5952a60c..9feb94f2 100644 --- a/src/main/java/com/aisip/OnO/backend/config/rabbitmq/consumer/ProblemAnalysisConsumer.java +++ b/src/main/java/com/aisip/OnO/backend/config/rabbitmq/consumer/ProblemAnalysisConsumer.java @@ -4,6 +4,7 @@ import com.aisip.OnO.backend.config.rabbitmq.RabbitMQConfig; import com.aisip.OnO.backend.config.rabbitmq.message.ProblemAnalysisMessage; import com.aisip.OnO.backend.problem.exception.ProblemErrorCase; +import com.aisip.OnO.backend.problem.service.ProblemAnalysisFailureService; import com.aisip.OnO.backend.problem.service.ProblemAnalysisService; import com.aisip.OnO.backend.util.ai.NonRetryableAnalysisException; import com.aisip.OnO.backend.util.webhook.DiscordWebhookNotificationService; @@ -23,6 +24,7 @@ public class ProblemAnalysisConsumer { private final ProblemAnalysisService analysisService; + private final ProblemAnalysisFailureService analysisFailureService; private final DiscordWebhookNotificationService discordWebhookNotificationService; /** @@ -95,6 +97,14 @@ public void handleAnalysisDLQ(ProblemAnalysisMessage message) { message.getRetryCount() ); + // DLQ 도달 = 모든 재시도 소진. PROCESSING 고착 방지를 위해 FAILED로 전이 + try { + analysisFailureService.markFailed(message.getProblemId(), "최대 재시도 횟수 초과 또는 큐 만료"); + } catch (Exception e) { + log.error("RabbitMQ DLQ markFailed failed - problemId: {}, error: {}", + message.getProblemId(), e.getMessage()); + } + try { discordWebhookNotificationService.sendErrorNotification( "RabbitMQ DLQ - GPT Analysis", From b5e2a6de47a2ecd87524bd7b63c586ec386304dc Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 30 Jun 2026 15:26:16 +0900 Subject: [PATCH 62/67] =?UTF-8?q?[Feat]=20=ED=8F=B4=EB=8D=94=20=EC=8D=B8?= =?UTF-8?q?=EB=84=A4=EC=9D=BC=20=EC=9D=91=EB=8B=B5=EC=97=90=20problemCount?= =?UTF-8?q?=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FolderThumbnailResponseDto에 problemCount 추가 - FolderRepository에 countProblemsByFolderIds() 벌크 쿼리 추가 - FolderService에 toFolderThumbnailDtos(), findProblemCountsByFolderIds() 헬퍼 추가 - 관련 Repository / Service / 통합 테스트 보완 --- .../backend/folder/dto/FolderResponseDto.java | 22 +++++-- .../dto/FolderThumbnailResponseDto.java | 10 +++- .../folder/repository/FolderRepository.java | 8 +++ .../backend/folder/service/FolderService.java | 60 +++++++++++++++---- .../integration/FolderApiIntegrationTest.java | 13 ++++ .../repository/FolderRepositoryTest.java | 22 ++++++- .../folder/service/FolderServiceTest.java | 6 ++ 7 files changed, 122 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/folder/dto/FolderResponseDto.java b/src/main/java/com/aisip/OnO/backend/folder/dto/FolderResponseDto.java index 42f0c30f..a0cebf5e 100644 --- a/src/main/java/com/aisip/OnO/backend/folder/dto/FolderResponseDto.java +++ b/src/main/java/com/aisip/OnO/backend/folder/dto/FolderResponseDto.java @@ -1,13 +1,13 @@ package com.aisip.OnO.backend.folder.dto; import com.aisip.OnO.backend.folder.entity.Folder; -import com.aisip.OnO.backend.problem.dto.ProblemResponseDto; import lombok.AccessLevel; import lombok.Builder; import org.jetbrains.annotations.NotNull; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; @Builder(access = AccessLevel.PRIVATE) public record FolderResponseDto ( @@ -29,15 +29,29 @@ public record FolderResponseDto ( LocalDateTime updateAt ) { public static FolderResponseDto from(@NotNull Folder folder, List problemIdList) { + return from(folder, problemIdList, Map.of()); + } + + public static FolderResponseDto from( + @NotNull Folder folder, + List problemIdList, + Map problemCountsByFolderId + ) { FolderThumbnailResponseDto parentFolder = folder.getParentFolder() != null - ? FolderThumbnailResponseDto.from(folder.getParentFolder()) + ? FolderThumbnailResponseDto.from( + folder.getParentFolder(), + problemCountsByFolderId.getOrDefault(folder.getParentFolder().getId(), 0L) + ) : null; List subFolderList = folder.getSubFolderList() != null ? folder.getSubFolderList() .stream() - .map(FolderThumbnailResponseDto::from) + .map(subFolder -> FolderThumbnailResponseDto.from( + subFolder, + problemCountsByFolderId.getOrDefault(subFolder.getId(), 0L) + )) .toList() : List.of(); @@ -52,4 +66,4 @@ public static FolderResponseDto from(@NotNull Folder folder, List problemI .updateAt(folder.getUpdatedAt()) .build(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/aisip/OnO/backend/folder/dto/FolderThumbnailResponseDto.java b/src/main/java/com/aisip/OnO/backend/folder/dto/FolderThumbnailResponseDto.java index 6a84e1ca..0ebff906 100644 --- a/src/main/java/com/aisip/OnO/backend/folder/dto/FolderThumbnailResponseDto.java +++ b/src/main/java/com/aisip/OnO/backend/folder/dto/FolderThumbnailResponseDto.java @@ -8,12 +8,18 @@ @Builder(access = AccessLevel.PRIVATE) public record FolderThumbnailResponseDto( Long folderId, - String folderName + String folderName, + Long problemCount ) { - public static FolderThumbnailResponseDto from(@NotNull Folder folder) { + public static FolderThumbnailResponseDto from(@NotNull Folder folder, Long problemCount) { return FolderThumbnailResponseDto.builder() .folderId(folder.getId()) .folderName(folder.getName()) + .problemCount(problemCount) .build(); } + + public static FolderThumbnailResponseDto from(@NotNull Folder folder) { + return from(folder, 0L); + } } diff --git a/src/main/java/com/aisip/OnO/backend/folder/repository/FolderRepository.java b/src/main/java/com/aisip/OnO/backend/folder/repository/FolderRepository.java index 00540d31..facee834 100644 --- a/src/main/java/com/aisip/OnO/backend/folder/repository/FolderRepository.java +++ b/src/main/java/com/aisip/OnO/backend/folder/repository/FolderRepository.java @@ -19,4 +19,12 @@ public interface FolderRepository extends JpaRepository, FolderRep @Modifying @Query("delete from Folder f where f.id in :folderIds") void deleteAllByIdIn(@Param("folderIds") Collection folderIds); + + @Query(""" + SELECT p.folder.id, COUNT(p.id) + FROM Problem p + WHERE p.folder.id IN :folderIds + GROUP BY p.folder.id + """) + List countProblemsByFolderIds(@Param("folderIds") Collection folderIds); } diff --git a/src/main/java/com/aisip/OnO/backend/folder/service/FolderService.java b/src/main/java/com/aisip/OnO/backend/folder/service/FolderService.java index 7e4c6b23..7a5ad7e8 100644 --- a/src/main/java/com/aisip/OnO/backend/folder/service/FolderService.java +++ b/src/main/java/com/aisip/OnO/backend/folder/service/FolderService.java @@ -8,7 +8,6 @@ import com.aisip.OnO.backend.folder.entity.Folder; import com.aisip.OnO.backend.folder.exception.FolderErrorCase; import com.aisip.OnO.backend.folder.repository.FolderRepository; -import com.aisip.OnO.backend.problem.dto.ProblemResponseDto; import com.aisip.OnO.backend.problem.service.ProblemService; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; @@ -55,7 +54,7 @@ public FolderResponseDto findRootFolder(Long userId) { log.info("userId : {} find root folder id: {}", userId, rootFolder.getId()); List problemIdList = folderRepository.findProblemIdsByFolder(rootFolder.getId()); - return FolderResponseDto.from(rootFolder, problemIdList); + return toFolderResponseDto(rootFolder, problemIdList); } @Transactional(readOnly = true) @@ -64,7 +63,7 @@ public FolderResponseDto findFolderForAdmin(Long folderId) { .orElseThrow(() -> new ApplicationException(FolderErrorCase.FOLDER_NOT_FOUND)); List problemIdList = folderRepository.findProblemIdsByFolder(folder.getId()); - return FolderResponseDto.from(folder, problemIdList); + return toFolderResponseDto(folder, problemIdList); } @Transactional(readOnly = true) @@ -72,7 +71,7 @@ public FolderResponseDto findFolder(Long folderId, Long userId) { Folder folder = findFolderWithDetailsOwnedByUser(folderId, userId); List problemIdList = folderRepository.findProblemIdsByFolder(folder.getId()); - return FolderResponseDto.from(folder, problemIdList); + return toFolderResponseDto(folder, problemIdList); } @Transactional(readOnly = true) @@ -94,7 +93,7 @@ public List findAllUserFolderThumbnails(Long userId) return folderList.isEmpty() ? List.of() - : folderList.stream().map(FolderThumbnailResponseDto::from).collect(Collectors.toList()); + : toFolderThumbnailDtos(folderList); } public List findAllUserFolders(Long userId) { @@ -105,7 +104,7 @@ public List findAllUserFolders(Long userId) { ? List.of() : folders.stream().map(folder -> { List problemIdList = folderRepository.findProblemIdsByFolder(folder.getId()); - return FolderResponseDto.from(folder, problemIdList); + return toFolderResponseDto(folder, problemIdList); }).toList(); } @@ -217,9 +216,7 @@ public CursorPageResponse findSubFoldersWithCursor(L List content = hasNext ? folders.subList(0, size) : folders; Long nextCursor = hasNext ? content.get(content.size() - 1).getId() : null; - List dtoList = content.stream() - .map(FolderThumbnailResponseDto::from) - .collect(Collectors.toList()); + List dtoList = toFolderThumbnailDtos(content); log.info("folderId: {} find subfolders with cursor: {}, size: {}, hasNext: {}", folderId, cursor, size, hasNext); return CursorPageResponse.of(dtoList, nextCursor, hasNext, size); @@ -253,11 +250,50 @@ public CursorPageResponse findAllUserFolderThumbnail List content = hasNext ? folders.subList(0, size) : folders; Long nextCursor = hasNext ? content.get(content.size() - 1).getId() : null; - List dtoList = content.stream() - .map(FolderThumbnailResponseDto::from) - .collect(Collectors.toList()); + List dtoList = toFolderThumbnailDtos(content); log.info("userId: {} find all folder thumbnails with cursor: {}, size: {}, hasNext: {}", userId, cursor, size, hasNext); return CursorPageResponse.of(dtoList, nextCursor, hasNext, size); } + + private FolderResponseDto toFolderResponseDto(Folder folder, List problemIdList) { + Set thumbnailFolderIds = new HashSet<>(); + if (folder.getParentFolder() != null) { + thumbnailFolderIds.add(folder.getParentFolder().getId()); + } + if (folder.getSubFolderList() != null) { + folder.getSubFolderList().stream() + .map(Folder::getId) + .forEach(thumbnailFolderIds::add); + } + + Map problemCountsByFolderId = findProblemCountsByFolderIds(thumbnailFolderIds); + return FolderResponseDto.from(folder, problemIdList, problemCountsByFolderId); + } + + private List toFolderThumbnailDtos(List folders) { + List folderIds = folders.stream() + .map(Folder::getId) + .toList(); + Map problemCountsByFolderId = findProblemCountsByFolderIds(folderIds); + + return folders.stream() + .map(folder -> FolderThumbnailResponseDto.from( + folder, + problemCountsByFolderId.getOrDefault(folder.getId(), 0L) + )) + .collect(Collectors.toList()); + } + + private Map findProblemCountsByFolderIds(Collection folderIds) { + if (folderIds == null || folderIds.isEmpty()) { + return Map.of(); + } + + return folderRepository.countProblemsByFolderIds(folderIds).stream() + .collect(Collectors.toMap( + row -> (Long) row[0], + row -> (Long) row[1] + )); + } } diff --git a/src/test/java/com/aisip/OnO/backend/folder/integration/FolderApiIntegrationTest.java b/src/test/java/com/aisip/OnO/backend/folder/integration/FolderApiIntegrationTest.java index 3724c2fe..968b7136 100644 --- a/src/test/java/com/aisip/OnO/backend/folder/integration/FolderApiIntegrationTest.java +++ b/src/test/java/com/aisip/OnO/backend/folder/integration/FolderApiIntegrationTest.java @@ -158,8 +158,10 @@ public void getFolderTest_Root() throws Exception { .andExpect(jsonPath("$.data.subFolderList.length()").value(folder.getSubFolderList().size())) .andExpect(jsonPath("$.data.subFolderList[0].folderId").value(folder.getSubFolderList().get(0).getId())) .andExpect(jsonPath("$.data.subFolderList[0].folderName").value(folder.getSubFolderList().get(0).getName())) + .andExpect(jsonPath("$.data.subFolderList[0].problemCount").value(folder.getSubFolderList().get(0).getProblemList().size())) .andExpect(jsonPath("$.data.subFolderList[1].folderId").value(folder.getSubFolderList().get(1).getId())) .andExpect(jsonPath("$.data.subFolderList[1].folderName").value(folder.getSubFolderList().get(1).getName())) + .andExpect(jsonPath("$.data.subFolderList[1].problemCount").value(folder.getSubFolderList().get(1).getProblemList().size())) .andExpect(jsonPath("$.data.problemIdList.length()").value(folder.getProblemList().size())) .andExpect(jsonPath("$.data.problemIdList[0]").value(folder.getProblemList().get(0).getId())) .andReturn(); @@ -181,11 +183,14 @@ public void getFolderTest_Internal() throws Exception { .andExpect(jsonPath("$.data.folderId").value(folder.getId())) .andExpect(jsonPath("$.data.folderName").value(folder.getName())) .andExpect(jsonPath("$.data.parentFolder.folderId").value(folder.getParentFolder().getId())) + .andExpect(jsonPath("$.data.parentFolder.problemCount").value(folder.getParentFolder().getProblemList().size())) .andExpect(jsonPath("$.data.subFolderList.length()").value(folder.getSubFolderList().size())) .andExpect(jsonPath("$.data.subFolderList[0].folderId").value(folder.getSubFolderList().get(0).getId())) .andExpect(jsonPath("$.data.subFolderList[0].folderName").value(folder.getSubFolderList().get(0).getName())) + .andExpect(jsonPath("$.data.subFolderList[0].problemCount").value(folder.getSubFolderList().get(0).getProblemList().size())) .andExpect(jsonPath("$.data.subFolderList[1].folderId").value(folder.getSubFolderList().get(1).getId())) .andExpect(jsonPath("$.data.subFolderList[1].folderName").value(folder.getSubFolderList().get(1).getName())) + .andExpect(jsonPath("$.data.subFolderList[1].problemCount").value(folder.getSubFolderList().get(1).getProblemList().size())) .andExpect(jsonPath("$.data.problemIdList.length()").value(folder.getProblemList().size())) .andExpect(jsonPath("$.data.problemIdList[0]").value(folder.getProblemList().get(0).getId())) .andReturn(); @@ -207,6 +212,7 @@ public void getFolderTest_External() throws Exception { .andExpect(jsonPath("$.data.folderId").value(folder.getId())) .andExpect(jsonPath("$.data.folderName").value(folder.getName())) .andExpect(jsonPath("$.data.parentFolder.folderId").value(folder.getParentFolder().getId())) + .andExpect(jsonPath("$.data.parentFolder.problemCount").value(folder.getParentFolder().getProblemList().size())) .andExpect(jsonPath("$.data.subFolderList.length()").value(0)) .andExpect(jsonPath("$.data.problemIdList.length()").value(folder.getProblemList().size())) .andExpect(jsonPath("$.data.problemIdList[0]").value(folder.getProblemList().get(0).getId())) @@ -233,8 +239,10 @@ public void getRootFolderTest_Exist() throws Exception { .andExpect(jsonPath("$.data.problemIdList.length()").value(rootFolder.getProblemList().size())) .andExpect(jsonPath("$.data.subFolderList[0].folderId").value(rootFolder.getSubFolderList().get(0).getId())) .andExpect(jsonPath("$.data.subFolderList[0].folderName").value(rootFolder.getSubFolderList().get(0).getName())) + .andExpect(jsonPath("$.data.subFolderList[0].problemCount").value(rootFolder.getSubFolderList().get(0).getProblemList().size())) .andExpect(jsonPath("$.data.subFolderList[1].folderId").value(rootFolder.getSubFolderList().get(1).getId())) .andExpect(jsonPath("$.data.subFolderList[1].folderName").value(rootFolder.getSubFolderList().get(1).getName())) + .andExpect(jsonPath("$.data.subFolderList[1].problemCount").value(rootFolder.getSubFolderList().get(1).getProblemList().size())) .andExpect(jsonPath("$.data.problemIdList[0]").value(rootFolder.getProblemList().get(0).getId())) .andReturn(); @@ -276,6 +284,7 @@ public void getAllUserFolderThumbnails_Test() throws Exception { Folder folder = folderList.get(i); assertThat(folder.getId().intValue()).isEqualTo( JsonPath.read(content, "$.data[" + i + "].folderId")); assertThat(folder.getName()).isEqualTo(JsonPath.read(content, "$.data[" + i + "].folderName")); + assertThat(folder.getProblemList().size()).isEqualTo(JsonPath.read(content, "$.data[" + i + "].problemCount")); } System.out.println("==== 응답 결과 ===="); @@ -306,6 +315,10 @@ public void getAllUserFolderDetails_Test() throws Exception { } assertThat(folder.getSubFolderList().size()).isEqualTo(JsonPath.read(content, "$.data[" + i + "].subFolderList.length()")); assertThat(folder.getProblemList().size()).isEqualTo(JsonPath.read(content, "$.data[" + i + "].problemIdList.length()")); + for (int j = 0; j < folder.getSubFolderList().size(); j++) { + assertThat(folder.getSubFolderList().get(j).getProblemList().size()) + .isEqualTo(JsonPath.read(content, "$.data[" + i + "].subFolderList[" + j + "].problemCount")); + } } System.out.println("==== 응답 결과 ===="); diff --git a/src/test/java/com/aisip/OnO/backend/folder/repository/FolderRepositoryTest.java b/src/test/java/com/aisip/OnO/backend/folder/repository/FolderRepositoryTest.java index 70bdeff0..9be89e43 100644 --- a/src/test/java/com/aisip/OnO/backend/folder/repository/FolderRepositoryTest.java +++ b/src/test/java/com/aisip/OnO/backend/folder/repository/FolderRepositoryTest.java @@ -24,7 +24,9 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -249,4 +251,22 @@ void findAllFoldersWithDetailsByUserIdTest () { assertThat(0L).isEqualTo(1L); } } -} \ No newline at end of file + + @Test + @DisplayName("폴더별 문제 수 집계 테스트") + void countProblemsByFolderIdsTest() { + List folderIds = folderList.stream() + .map(Folder::getId) + .toList(); + + Map problemCountsByFolderId = folderRepository.countProblemsByFolderIds(folderIds).stream() + .collect(Collectors.toMap( + row -> (Long) row[0], + row -> (Long) row[1] + )); + + for (Folder folder : folderList) { + assertThat(problemCountsByFolderId.get(folder.getId())).isEqualTo(2L); + } + } +} diff --git a/src/test/java/com/aisip/OnO/backend/folder/service/FolderServiceTest.java b/src/test/java/com/aisip/OnO/backend/folder/service/FolderServiceTest.java index 66e0acc0..ca1140e8 100644 --- a/src/test/java/com/aisip/OnO/backend/folder/service/FolderServiceTest.java +++ b/src/test/java/com/aisip/OnO/backend/folder/service/FolderServiceTest.java @@ -157,7 +157,9 @@ void findRootFolder() { assertThat(folderResponseDto.folderName()).isEqualTo(folderList.get(0).getName()); assertThat(folderResponseDto.parentFolder()).isNull(); assertThat(folderResponseDto.subFolderList().get(0).folderId()).isEqualTo(folderList.get(1).getId()); + assertThat(folderResponseDto.subFolderList().get(0).problemCount()).isEqualTo((long) folderList.get(1).getProblemList().size()); assertThat(folderResponseDto.subFolderList().get(1).folderId()).isEqualTo(folderList.get(2).getId()); + assertThat(folderResponseDto.subFolderList().get(1).problemCount()).isEqualTo((long) folderList.get(2).getProblemList().size()); assertThat(folderResponseDto.problemIdList().get(0)).isEqualTo(problemList.get(0).problemId()); assertThat(folderResponseDto.problemIdList().get(1)).isEqualTo(problemList.get(1).problemId()); } @@ -176,8 +178,11 @@ void findFolder() { assertThat(folderResponseDto.folderId()).isEqualTo(folderList.get(1).getId()); assertThat(folderResponseDto.folderName()).isEqualTo(folderList.get(1).getName()); assertThat(folderResponseDto.parentFolder().folderId()).isEqualTo(parentFolderId); + assertThat(folderResponseDto.parentFolder().problemCount()).isEqualTo((long) folderList.get(0).getProblemList().size()); assertThat(folderResponseDto.subFolderList().get(0).folderId()).isEqualTo(folderList.get(3).getId()); + assertThat(folderResponseDto.subFolderList().get(0).problemCount()).isEqualTo((long) folderList.get(3).getProblemList().size()); assertThat(folderResponseDto.subFolderList().get(1).folderId()).isEqualTo(folderList.get(4).getId()); + assertThat(folderResponseDto.subFolderList().get(1).problemCount()).isEqualTo((long) folderList.get(4).getProblemList().size()); assertThat(folderResponseDto.problemIdList().get(0)).isEqualTo(problemList.get(2).problemId()); assertThat(folderResponseDto.problemIdList().get(1)).isEqualTo(problemList.get(3).problemId()); } @@ -222,6 +227,7 @@ void findAllUserFolderThumbnails() { for (int i = 0; i < folderThumbnailResponseDtoList.size(); i++) { assertThat(folderThumbnailResponseDtoList.get(i).folderId()).isEqualTo(folderList.get(i).getId()); assertThat(folderThumbnailResponseDtoList.get(i).folderName()).isEqualTo(folderList.get(i).getName()); + assertThat(folderThumbnailResponseDtoList.get(i).problemCount()).isEqualTo((long) folderList.get(i).getProblemList().size()); } } From ddc0e1e7bd84169efd8e1cfc6d132072cf176034 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 30 Jun 2026 15:26:22 +0900 Subject: [PATCH 63/67] =?UTF-8?q?[Feat]=20StudyRoom=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=EC=97=90=20=EB=A9=A4=EB=B2=84=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=EC=99=80=20?= =?UTF-8?q?=EC=98=A4=EB=8A=98=20=EC=97=B0=EC=8A=B5=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - StudyRoomMemberSummary record 추가 (userId, profileImageUrl, practicedToday) - StudyRoomListResponse에 members 필드 추가 - 멤버카운트 별도 쿼리 제거, 멤버 목록 단건 조회로 통합 --- .../backend/studyroom/dto/StudyRoomDtos.java | 6 ++++- .../studyroom/service/StudyRoomMapper.java | 27 ++++++++++++++----- .../studyroom/service/StudyRoomService.java | 13 ++++----- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java index 599ad111..18739ecf 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/dto/StudyRoomDtos.java @@ -36,9 +36,13 @@ public record SharedProblemCreateRequest(Long problemId, String comment) { public record SharedProblemCommentRequest(String content) { } + public record StudyRoomMemberSummary(Long userId, String profileImageUrl, boolean practicedToday) { + } + public record StudyRoomListResponse(Long roomId, String name, Long hostUserId, int memberCount, String thumbnailUrl, boolean hasUnreadReport, - int todayPracticeMemberCount, int todayPracticeCount) { + int todayPracticeMemberCount, int todayPracticeCount, + List members) { } public record StudyRoomDetailResponse(Long roomId, String name, Long hostUserId, String thumbnailUrl, diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomMapper.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomMapper.java index baf73be0..07d6390c 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomMapper.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomMapper.java @@ -3,13 +3,13 @@ import com.aisip.OnO.backend.mission.entity.UserMissionStatus; import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; import com.aisip.OnO.backend.studyroom.dto.StudyRoomStats; -import com.aisip.OnO.backend.studyroom.dto.StudyRoomTodayPracticeSummary; import com.aisip.OnO.backend.studyroom.entity.StudyRoom; import com.aisip.OnO.backend.studyroom.entity.StudyRoomMember; import com.aisip.OnO.backend.studyroom.repository.StudyRoomWeeklyReportReadRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -19,18 +19,33 @@ public class StudyRoomMapper { private final StudyRoomWeeklyReportReadRepository reportReadRepository; - public StudyRoomListResponse toListResponse(StudyRoomMember member, int memberCount, - StudyRoomTodayPracticeSummary todayPracticeSummary) { + public StudyRoomListResponse toListResponse(StudyRoomMember member, List roomMembers, + Map todayPracticeCountByUserId) { StudyRoom room = member.getRoom(); + int todayPracticeMemberCount = 0; + int todayPracticeCount = 0; + List memberSummaries = new ArrayList<>(roomMembers.size()); + for (StudyRoomMember m : roomMembers) { + int count = todayPracticeCountByUserId.getOrDefault(m.getUser().getId(), 0); + boolean practicedToday = count > 0; + if (practicedToday) todayPracticeMemberCount++; + todayPracticeCount += count; + memberSummaries.add(new StudyRoomMemberSummary( + m.getUser().getId(), + m.getUser().getProfileImageUrl(), + practicedToday + )); + } return new StudyRoomListResponse( room.getId(), room.getName(), room.getHostUserId(), - memberCount, + roomMembers.size(), room.getThumbnailUrl(), reportReadRepository.existsUnreadReport(room.getId(), member.getUser().getId()), - todayPracticeSummary.todayPracticeMemberCount(), - todayPracticeSummary.todayPracticeCount() + todayPracticeMemberCount, + todayPracticeCount, + memberSummaries ); } diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java index 19bf9715..920be37f 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomService.java @@ -3,7 +3,6 @@ import com.aisip.OnO.backend.common.exception.ApplicationException; import com.aisip.OnO.backend.studyroom.dto.StudyRoomDtos.*; import com.aisip.OnO.backend.studyroom.dto.StudyRoomStats; -import com.aisip.OnO.backend.studyroom.dto.StudyRoomTodayPracticeSummary; import com.aisip.OnO.backend.studyroom.entity.StudyRoom; import com.aisip.OnO.backend.studyroom.entity.StudyRoomMember; import com.aisip.OnO.backend.studyroom.entity.StudyRoomMemberRole; @@ -52,14 +51,16 @@ public List getMyRooms(Long userId) { if (roomIds.isEmpty()) { return List.of(); } - Map memberCounts = memberRepository.countMembersByRoomIds(roomIds).stream() - .collect(java.util.stream.Collectors.toMap(row -> (Long) row[0], row -> Math.toIntExact((Long) row[1]))); - Map todayPracticeSummaries = statsService.getTodayPracticeSummariesByRoomIds(roomIds); + List allRoomMembers = memberRepository.findAllWithRoomAndUserByRoomIds(roomIds); + Map> membersByRoomId = allRoomMembers.stream() + .collect(java.util.stream.Collectors.groupingBy(m -> m.getRoom().getId())); + List allUserIds = allRoomMembers.stream().map(m -> m.getUser().getId()).distinct().toList(); + Map todayPracticeCountByUserId = statsService.getTodayPracticeCounts(allUserIds); return memberships.stream() .map(member -> mapper.toListResponse( member, - memberCounts.getOrDefault(member.getRoom().getId(), 0), - todayPracticeSummaries.getOrDefault(member.getRoom().getId(), StudyRoomTodayPracticeSummary.empty()) + membersByRoomId.getOrDefault(member.getRoom().getId(), List.of()), + todayPracticeCountByUserId )) .toList(); } From 28d0ffc75d4fef6b31eedf89041f499603c9b17e Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 30 Jun 2026 15:26:26 +0900 Subject: [PATCH 64/67] =?UTF-8?q?[Chore]=20refresh=5Ftoken=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=BC=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(V17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/db/migration/V17__add_refresh_token_index.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/main/resources/db/migration/V17__add_refresh_token_index.sql diff --git a/src/main/resources/db/migration/V17__add_refresh_token_index.sql b/src/main/resources/db/migration/V17__add_refresh_token_index.sql new file mode 100644 index 00000000..6d778975 --- /dev/null +++ b/src/main/resources/db/migration/V17__add_refresh_token_index.sql @@ -0,0 +1,2 @@ +ALTER TABLE refresh_token + ADD INDEX idx_refresh_token_token (refresh_token(255)); From ae1122002af760641784e2fad74e37e75b0dcb4f Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 30 Jun 2026 15:55:16 +0900 Subject: [PATCH 65/67] =?UTF-8?q?[Fix]=20=EA=B2=80=EC=A6=9D=EB=90=9C=205?= =?UTF-8?q?=EA=B0=9C=20=EC=9D=B4=EC=8A=88=20=EC=9D=BC=EA=B4=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [Fix] FCM RabbitMQ enqueue를 TransactionSynchronization.afterCommit으로 이동 — REQUIRES_NEW 롤백 시 중복 발송 방지 - [Fix] AddProblemImageUrlsRequest.imageDataList, ImageUrlsRequest.imageUrls에 @NotNull 추가 및 컨트롤러 @Validated 적용 — null 전송 시 NPE → 500 방지 - [Perf] FolderService.findAllUserFolders 2N+1 쿼리 → 배치 쿼리 전환 (findProblemIdsByFolderIds 신규 추가) - [Perf] StudyRoomChallengeService.checkAndNotifyChallengeCompletionForUser에 stats 캐시 도입 — 동일 range 챌린지의 중복 DB 쿼리 제거 - [Feat] Presigned URL 발급 엔드포인트에 일일 200회 Rate Limit 적용 Co-Authored-By: Claude Sonnet 4.6 --- .../repository/FolderRepositoryCustom.java | 4 + .../repository/FolderRepositoryImpl.java | 21 +++++ .../backend/folder/service/FolderService.java | 25 ++++-- .../problem/controller/ProblemController.java | 3 +- .../dto/AddProblemImageUrlsRequest.java | 4 +- .../controller/ProblemSolveController.java | 6 +- .../service/StudyRoomChallengeService.java | 78 ++++++++++++++++++- .../controller/FileUploadController.java | 2 + 8 files changed, 131 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/folder/repository/FolderRepositoryCustom.java b/src/main/java/com/aisip/OnO/backend/folder/repository/FolderRepositoryCustom.java index 39679984..4d9715b4 100644 --- a/src/main/java/com/aisip/OnO/backend/folder/repository/FolderRepositoryCustom.java +++ b/src/main/java/com/aisip/OnO/backend/folder/repository/FolderRepositoryCustom.java @@ -2,7 +2,9 @@ import com.aisip.OnO.backend.folder.entity.Folder; +import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Optional; public interface FolderRepositoryCustom { @@ -17,6 +19,8 @@ public interface FolderRepositoryCustom { List findProblemIdsByFolder(Long folderId); + Map> findProblemIdsByFolderIds(Collection folderIds); + /** * 커서 기반 하위 폴더 조회 * @param folderId 부모 폴더 ID diff --git a/src/main/java/com/aisip/OnO/backend/folder/repository/FolderRepositoryImpl.java b/src/main/java/com/aisip/OnO/backend/folder/repository/FolderRepositoryImpl.java index e62b678e..172bd034 100644 --- a/src/main/java/com/aisip/OnO/backend/folder/repository/FolderRepositoryImpl.java +++ b/src/main/java/com/aisip/OnO/backend/folder/repository/FolderRepositoryImpl.java @@ -2,11 +2,15 @@ import com.aisip.OnO.backend.folder.entity.Folder; import com.aisip.OnO.backend.folder.entity.QFolder; +import com.querydsl.core.Tuple; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; +import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import static com.aisip.OnO.backend.folder.entity.QFolder.folder; import static com.aisip.OnO.backend.problem.entity.QProblem.problem; @@ -73,6 +77,23 @@ public List findProblemIdsByFolder(Long folderId) { .fetch(); } + @Override + public Map> findProblemIdsByFolderIds(Collection folderIds) { + if (folderIds == null || folderIds.isEmpty()) { + return Map.of(); + } + List rows = queryFactory + .select(problem.folder.id, problem.id) + .from(problem) + .where(problem.folder.id.in(folderIds)) + .orderBy(problem.folder.id.asc(), problem.id.asc()) + .fetch(); + return rows.stream().collect(Collectors.groupingBy( + t -> t.get(problem.folder.id), + Collectors.mapping(t -> t.get(problem.id), Collectors.toList()) + )); + } + @Override public List findSubFoldersWithCursor(Long folderId, Long cursor, int size) { var query = queryFactory diff --git a/src/main/java/com/aisip/OnO/backend/folder/service/FolderService.java b/src/main/java/com/aisip/OnO/backend/folder/service/FolderService.java index 7a5ad7e8..d6f35370 100644 --- a/src/main/java/com/aisip/OnO/backend/folder/service/FolderService.java +++ b/src/main/java/com/aisip/OnO/backend/folder/service/FolderService.java @@ -98,14 +98,27 @@ public List findAllUserFolderThumbnails(Long userId) public List findAllUserFolders(Long userId) { List folders = folderRepository.findAllFoldersWithDetailsByUserId(userId); + if (folders.isEmpty()) { + return List.of(); + } + + List folderIds = folders.stream().map(Folder::getId).toList(); + Map> problemIdsByFolder = folderRepository.findProblemIdsByFolderIds(folderIds); + + Set thumbnailFolderIds = new HashSet<>(); + for (Folder f : folders) { + if (f.getParentFolder() != null) thumbnailFolderIds.add(f.getParentFolder().getId()); + if (f.getSubFolderList() != null) f.getSubFolderList().forEach(sf -> thumbnailFolderIds.add(sf.getId())); + } + Map problemCounts = findProblemCountsByFolderIds(thumbnailFolderIds); log.info("userId : {} find All user folders", userId); - return folders.isEmpty() - ? List.of() - : folders.stream().map(folder -> { - List problemIdList = folderRepository.findProblemIdsByFolder(folder.getId()); - return toFolderResponseDto(folder, problemIdList); - }).toList(); + return folders.stream() + .map(folder -> FolderResponseDto.from( + folder, + problemIdsByFolder.getOrDefault(folder.getId(), List.of()), + problemCounts)) + .toList(); } private void createDefaultSubFolder(Folder rootFolder, Long userId) { diff --git a/src/main/java/com/aisip/OnO/backend/problem/controller/ProblemController.java b/src/main/java/com/aisip/OnO/backend/problem/controller/ProblemController.java index e30819a8..612c3686 100644 --- a/src/main/java/com/aisip/OnO/backend/problem/controller/ProblemController.java +++ b/src/main/java/com/aisip/OnO/backend/problem/controller/ProblemController.java @@ -17,6 +17,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.propertyeditors.CustomNumberEditor; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -170,7 +171,7 @@ public CommonResponse uploadProblemImages( @PostMapping("/{problemId}/imageData/urls") public CommonResponse addProblemImageDataUrls( @PathVariable("problemId") Long problemId, - @RequestBody AddProblemImageUrlsRequest request + @Validated @RequestBody AddProblemImageUrlsRequest request ) { Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); problemService.addImageDataUrls(problemId, userId, request); diff --git a/src/main/java/com/aisip/OnO/backend/problem/dto/AddProblemImageUrlsRequest.java b/src/main/java/com/aisip/OnO/backend/problem/dto/AddProblemImageUrlsRequest.java index 9e52a953..ebe45fbd 100644 --- a/src/main/java/com/aisip/OnO/backend/problem/dto/AddProblemImageUrlsRequest.java +++ b/src/main/java/com/aisip/OnO/backend/problem/dto/AddProblemImageUrlsRequest.java @@ -1,8 +1,10 @@ package com.aisip.OnO.backend.problem.dto; +import jakarta.validation.constraints.NotNull; + import java.util.List; -public record AddProblemImageUrlsRequest(List imageDataList) { +public record AddProblemImageUrlsRequest(@NotNull List imageDataList) { public record ImageUrlItem(String imageUrl, String problemImageType) {} } diff --git a/src/main/java/com/aisip/OnO/backend/problemsolve/controller/ProblemSolveController.java b/src/main/java/com/aisip/OnO/backend/problemsolve/controller/ProblemSolveController.java index 4c36792b..7f309d7c 100644 --- a/src/main/java/com/aisip/OnO/backend/problemsolve/controller/ProblemSolveController.java +++ b/src/main/java/com/aisip/OnO/backend/problemsolve/controller/ProblemSolveController.java @@ -5,9 +5,11 @@ import com.aisip.OnO.backend.problemsolve.dto.ProblemSolveResponseDto; import com.aisip.OnO.backend.problemsolve.dto.ProblemSolveUpdateDto; import com.aisip.OnO.backend.problemsolve.service.ProblemSolveService; +import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -92,14 +94,14 @@ public CommonResponse uploadProblemSolveImages( @PostMapping("/{problemSolveId}/image-urls") public CommonResponse addProblemSolveImageUrls( @PathVariable("problemSolveId") Long problemSolveId, - @RequestBody ImageUrlsRequest request) { + @Validated @RequestBody ImageUrlsRequest request) { Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); problemSolveService.addImageUrls(problemSolveId, userId, request.imageUrls()); return CommonResponse.success(null); } - public record ImageUrlsRequest(List imageUrls) {} + public record ImageUrlsRequest(@NotNull List imageUrls) {} // 복습 기록 수정 @PatchMapping("") diff --git a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java index dbda23f2..0ee020b5 100644 --- a/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java +++ b/src/main/java/com/aisip/OnO/backend/studyroom/service/StudyRoomChallengeService.java @@ -14,6 +14,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; import java.time.LocalDate; import java.time.LocalDateTime; @@ -124,7 +126,15 @@ private ChallengeResponse toResponse(StudyRoomChallenge challenge, List 0) { challenge.updateStatus(status); - notifyChallengeCompleted(challenge, members); + // 트랜잭션 커밋 후 FCM 발송 — 롤백 시 중복 발송 방지 + final StudyRoomChallenge committedChallenge = challenge; + final List committedMembers = members; + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + notifyChallengeCompleted(committedChallenge, committedMembers); + } + }); } } else { challenge.updateStatus(status); @@ -254,13 +264,77 @@ public void checkAndNotifyChallengeCompletionForUser(Long userId) { } Map> membersByRoom = new HashMap<>(); + Map statsCache = new HashMap<>(); for (StudyRoomChallenge challenge : inProgressChallenges) { Long roomId = challenge.getRoom().getId(); membersByRoom.computeIfAbsent(roomId, id -> memberRepository.findAllWithUserByRoomId(id)); - toResponse(challenge, membersByRoom.get(roomId), true); + evaluateCompletionAndNotify(challenge, membersByRoom.get(roomId), statsCache); } } + private void evaluateCompletionAndNotify( + StudyRoomChallenge challenge, + List members, + Map statsCache) { + List userIds = members.stream().map(m -> m.getUser().getId()).toList(); + AggregationRange range = resolveAggregationRange(challenge, LocalDateTime.now()); + LocalDate streakBaseDate = min(LocalDate.now(), challenge.getEndAt().toLocalDate()); + boolean needsAttendance = isAttendanceMetric(challenge.getMetric()); + + StatsKey key = new StatsKey(userIds, streakBaseDate, challenge.getStartAt().toLocalDate(), range.start(), range.end()); + CachedStats cached = statsCache.computeIfAbsent(key, k -> { + Map streaks = statsService.currentStreaks(k.userIds(), k.streakBase(), k.challengeStart()); + Map stats = statsService.getStats(k.userIds(), k.rangeStart(), k.rangeEnd(), streaks); + Map attendance = statsService.attendanceDayCounts(k.userIds(), k.rangeStart(), k.rangeEnd()); + return new CachedStats(streaks, stats, attendance); + }); + + final CachedStats finalCached = cached; + List memberProgress = challenge.getType() == StudyRoomChallengeType.GROUP + ? List.of() + : members.stream() + .map(member -> { + int current = metricValue(challenge.getMetric(), + finalCached.stats().get(member.getUser().getId()), + needsAttendance ? finalCached.attendanceDays().getOrDefault(member.getUser().getId(), 0) : 0); + return new ChallengeMemberProgressResponse(member.getUser().getId(), member.getUser().getName(), + member.getUser().getProfileImageUrl(), current, current >= challenge.getTargetValue()); + }) + .toList(); + Integer groupCurrent = challenge.getType() == StudyRoomChallengeType.GROUP + ? userIds.stream() + .mapToInt(uid -> metricValue(challenge.getMetric(), finalCached.stats().get(uid), + needsAttendance ? finalCached.attendanceDays().getOrDefault(uid, 0) : 0)) + .sum() + : null; + StudyRoomChallengeStatus status = effectiveStatus(challenge, memberProgress, groupCurrent); + + if (challenge.getStatus() != status) { + if (status == StudyRoomChallengeStatus.COMPLETED) { + int updated = challengeRepository.tryTransitionFromInProgress(challenge.getId(), status, LocalDateTime.now()); + if (updated > 0) { + challenge.updateStatus(status); + final StudyRoomChallenge committedChallenge = challenge; + final List committedMembers = members; + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + notifyChallengeCompleted(committedChallenge, committedMembers); + } + }); + } + } else { + challenge.updateStatus(status); + } + } + } + + private record StatsKey(List userIds, LocalDate streakBase, LocalDate challengeStart, + LocalDateTime rangeStart, LocalDateTime rangeEnd) {} + + private record CachedStats(Map streaks, Map stats, + Map attendanceDays) {} + private void notifyChallengeCompleted(StudyRoomChallenge challenge, List members) { NotificationRequestDto dto = new NotificationRequestDto(null, "챌린지 달성! 🎉", diff --git a/src/main/java/com/aisip/OnO/backend/util/fileupload/controller/FileUploadController.java b/src/main/java/com/aisip/OnO/backend/util/fileupload/controller/FileUploadController.java index 2d7e5a81..3d6fd507 100644 --- a/src/main/java/com/aisip/OnO/backend/util/fileupload/controller/FileUploadController.java +++ b/src/main/java/com/aisip/OnO/backend/util/fileupload/controller/FileUploadController.java @@ -1,5 +1,6 @@ package com.aisip.OnO.backend.util.fileupload.controller; +import com.aisip.OnO.backend.common.ratelimit.RateLimit; import com.aisip.OnO.backend.common.response.CommonResponse; import com.aisip.OnO.backend.util.fileupload.dto.PresignedUrlResponse; import com.aisip.OnO.backend.util.fileupload.service.FileUploadService; @@ -42,6 +43,7 @@ public CommonResponse> uploadMultipleImageFiles( // 앱이 S3에 직접 업로드할 수 있는 Presigned URL 발급 // count: 발급할 URL 수 (최대 20) // contentType: 업로드할 이미지 MIME 타입 (예: image/jpeg) + @RateLimit(key = "presigned_url", limitPerDay = 200) @GetMapping("/presigned-urls") public CommonResponse> getPresignedUrls( @RequestParam(defaultValue = "1") int count, From 3ebe23e8fe500c937a2d4ce39c06afc5f1295641 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 30 Jun 2026 17:33:00 +0900 Subject: [PATCH 66/67] =?UTF-8?q?[Chore]=20=EC=97=90=EC=9D=B4=EC=A0=84?= =?UTF-8?q?=ED=8A=B8=20=EB=AA=85=EB=A0=B9=20=EC=A0=95=EC=B1=85=20=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0=20-=20commit=20=EC=88=98=ED=96=89=20pr=20push=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/claude-guide.md | 10 ++++++---- .claude/commands/commit.md | 14 +++++++++----- .claude/commands/pr.md | 13 +++++++++---- .codex/USAGE.md | 9 +++++---- .codex/skills/commit/SKILL.md | 12 +++++++----- .codex/skills/commit/agents/openai.yaml | 4 ++-- .codex/skills/pr/SKILL.md | 3 ++- .codex/skills/pr/agents/openai.yaml | 4 ++-- AGENTS.md | 7 +++++-- CLAUDE.md | 2 +- .../static/images/ScreenShot/Emoji.png | Bin 0 -> 129386 bytes .../images/ScreenShot/StudyRoomActivity.png | Bin 0 -> 85752 bytes .../images/ScreenShot/StudyRoomChallenge.png | Bin 0 -> 81137 bytes .../images/ScreenShot/StudyRoomRanking.png | Bin 0 -> 93349 bytes .../images/ScreenShot/StudyRoomReport.png | Bin 0 -> 74097 bytes .../images/ScreenShot/StudyRoomShare.png | Bin 0 -> 79585 bytes .../ScreenShot/StudyRoomShareComment.png | Bin 0 -> 83060 bytes .../images/ScreenShot/StudyRoomShareDetail.png | Bin 0 -> 79923 bytes .../images/ScreenShot/StudyRoomThumbnail.png | Bin 0 -> 121237 bytes 19 files changed, 48 insertions(+), 30 deletions(-) create mode 100644 src/main/resources/static/images/ScreenShot/Emoji.png create mode 100644 src/main/resources/static/images/ScreenShot/StudyRoomActivity.png create mode 100644 src/main/resources/static/images/ScreenShot/StudyRoomChallenge.png create mode 100644 src/main/resources/static/images/ScreenShot/StudyRoomRanking.png create mode 100644 src/main/resources/static/images/ScreenShot/StudyRoomReport.png create mode 100644 src/main/resources/static/images/ScreenShot/StudyRoomShare.png create mode 100644 src/main/resources/static/images/ScreenShot/StudyRoomShareComment.png create mode 100644 src/main/resources/static/images/ScreenShot/StudyRoomShareDetail.png create mode 100644 src/main/resources/static/images/ScreenShot/StudyRoomThumbnail.png diff --git a/.claude/claude-guide.md b/.claude/claude-guide.md index 60b881c0..907e9d00 100644 --- a/.claude/claude-guide.md +++ b/.claude/claude-guide.md @@ -220,9 +220,9 @@ Claude Code에서 `/커맨드명 인자` 형식으로 호출합니다. --- -### `/commit` — 커밋 단위 제안 +### `/commit` — 커밋 생성 -**목적**: 현재 변경분을 논리 단위로 나누고 커밋 메시지 초안을 제시한다. 실제 커밋은 하지 않음. +**목적**: 현재 변경분을 논리 단위로 나누고 OnO 커밋 메시지 형식으로 실제 커밋까지 수행한다. ``` /commit # 현재 변경 전체 분석 @@ -232,7 +232,9 @@ Claude Code에서 `/커맨드명 인자` 형식으로 호출합니다. **동작 흐름**: 1. `git diff HEAD` + `git status`로 변경 전체 파악 2. 논리 단위 분할 (각 단위는 독립 빌드 가능, 단일 목적) -3. 단위별 커밋 메시지 제안 + 실행할 git 명령어 제시 +3. 단위별 커밋 메시지 결정 +4. 관련 파일만 staging한 뒤 논리 단위별로 `git commit` 실행 +5. 생성된 커밋 해시와 남은 변경사항 보고 **커밋 메시지 형식**: ``` @@ -435,7 +437,7 @@ allowlist에 없는 Bash 명령(실제 커밋, 파일 쓰기, 서버 시작 등) /feat 문제 등록 태그 복수 추가 # → 컴파일 자동 확인 -# → 커밋 메시지 제안 +# → 커밋 전 요약 보고 # 3. 테스트 /test ProblemService diff --git a/.claude/commands/commit.md b/.claude/commands/commit.md index aa42997b..adfa4b16 100644 --- a/.claude/commands/commit.md +++ b/.claude/commands/commit.md @@ -1,11 +1,11 @@ --- -description: 현재 변경분을 논리 단위로 분할하고 커밋 메시지 제안 (실제 커밋 X). +description: 현재 변경분을 논리 단위로 분할하고 OnO 형식으로 실제 커밋까지 수행. argument-hint: "[추가 맥락 — 선택]" --- -# /commit — 커밋 단위 제안 +# /commit — 커밋 생성 -**실제 커밋 실행 안 함. 제안만.** +**변경 범위를 확인한 뒤 논리 단위별로 `git add`와 `git commit`까지 수행한다.** ## 맥락: $ARGUMENTS @@ -17,7 +17,7 @@ argument-hint: "[추가 맥락 — 선택]" - 각 단위는 독립적으로 빌드 가능하고 단일 목적. - 관련 없는 변경이 섞이지 않도록. -3. 각 단위별 커밋 메시지 제안: +3. 각 단위별 커밋 메시지 결정: ``` [태그] 한 줄 요약 - 상세 1 @@ -25,4 +25,8 @@ argument-hint: "[추가 맥락 — 선택]" ``` 태그: `[Feat]` / `[Fix]` / `[Refactor]` / `[Chore]` / `[Test]` / `[Docs]` / `[Perf]` -4. 제안된 순서대로 커밋하는 명령어도 함께 제시. +4. 관련 없는 변경이 섞이지 않도록 단위별로 staging 대상을 분리한다. + +5. 각 단위마다 `git add` 후 `git commit`을 실행한다. + +6. 완료 후 생성된 커밋 해시, 커밋 메시지, 포함 파일을 보고한다. diff --git a/.claude/commands/pr.md b/.claude/commands/pr.md index 7a93a813..46ffea7e 100644 --- a/.claude/commands/pr.md +++ b/.claude/commands/pr.md @@ -22,7 +22,7 @@ argument-hint: "<추가 설명 (선택)>" - `git log develop..HEAD --oneline`으로 develop 대비 커밋 목록 확인 - `git diff develop...HEAD`로 변경 내용 파악 - `git status`로 미커밋 변경사항 확인 (있으면 사용자에게 명시) -- 원격에 push 여부 확인 (`git log origin/$(git branch --show-current)..HEAD 2>/dev/null`로 미push 커밋 확인) +- 원격 브랜치 존재 여부와 미push 커밋 확인 ### 2. 변경 내용 분석 커밋 로그와 diff를 바탕으로 다음을 파악한다: @@ -111,9 +111,14 @@ PR을 생성하기 전에 다음 항목을 사용자에게 보여주고 확인 사용자가 수정을 요청하면 반영한 뒤 재확인한다. 승인하면 다음 단계로 진행한다. -### 7. 원격 브랜치 push 확인 -미push 커밋이 있으면 사용자에게 알리고, push를 완료한 뒤 진행할 것을 안내한다. -push가 완료된 것을 확인한 후 PR을 생성한다. +### 7. 원격 브랜치 push +원격 브랜치가 없거나 미push 커밋이 있으면 직접 push한 뒤 PR 생성을 진행한다. + +```bash +git push -u origin $(git branch --show-current) +``` + +push 실패 시 인증, 권한, 네트워크 오류를 보고하고 PR 생성을 중단한다. ### 8. GitHub PR 생성 `mcp__github__.create_pull_request` 도구를 사용해 PR을 생성한다: diff --git a/.codex/USAGE.md b/.codex/USAGE.md index 1096871d..4377d4f3 100644 --- a/.codex/USAGE.md +++ b/.codex/USAGE.md @@ -191,17 +191,17 @@ $check abc1234 이후 변경사항 검토해줘 ### commit -현재 변경사항의 커밋 단위와 메시지만 제안할 때 사용한다. 실제 커밋은 명시 요청 전까지 실행하지 않는다. +현재 변경사항의 커밋 단위와 메시지를 정리하고 실제 커밋까지 수행할 때 사용한다. ```text -$commit 현재 변경사항 커밋 단위 추천해줘 +$commit 현재 변경사항 커밋해줘 ``` 수행 방식: - `git status`, `git diff HEAD`로 변경 전체를 파악한다. - 서로 독립적인 논리 단위로 나눈다. -- OnO 커밋 메시지 형식으로 제안한다. +- 논리 단위별로 필요한 파일만 `git add`하고 OnO 커밋 메시지 형식으로 `git commit`을 수행한다. ```text [Feat] 구현 타이틀 - 구현 상세1 구현 상세2 @@ -267,7 +267,7 @@ $migrate review_schedule에 알림 실패 사유 컬럼 추가 ### pr -기준 커밋부터 현재까지의 변경사항으로 PR 제목과 본문 초안을 작성할 때 사용한다. +현재 브랜치의 개발 내역을 `develop` 브랜치로 merge 요청하는 GitHub PR로 직접 생성할 때 사용한다. ```text $pr abc1234 @@ -276,6 +276,7 @@ $pr abc1234 수행 방식: - `git log`, `git diff`, `git status`로 커밋된 변경과 미커밋 변경을 분리한다. +- 원격 브랜치가 없거나 미push 커밋이 있으면 `git push -u origin 현재브랜치`로 직접 push한다. - `.github/PULL_REQUEST_TEMPLATE.md`를 읽고 같은 구조로 작성한다. - 사용자 영향, API 호환성, DB migration, 인증/권한, 검증 결과, 배포 리스크, 롤백 방법을 포함한다. diff --git a/.codex/skills/commit/SKILL.md b/.codex/skills/commit/SKILL.md index 178da7ad..1d3683d2 100644 --- a/.codex/skills/commit/SKILL.md +++ b/.codex/skills/commit/SKILL.md @@ -1,6 +1,6 @@ --- name: commit -description: 현재 변경사항의 커밋 단위와 OnO 커밋 메시지를 제안할 때 사용. 인자가 없어도 현재 git 변경사항을 대상으로 한다. 실제 git commit은 사용자가 명시적으로 요청하기 전까지 실행하지 않는다. +description: 현재 변경사항의 커밋 단위와 OnO 커밋 메시지를 정리하고 실제 git commit까지 수행할 때 사용. 인자가 없어도 현재 git 변경사항을 대상으로 한다. --- # OnO Commit @@ -10,8 +10,9 @@ description: 현재 변경사항의 커밋 단위와 OnO 커밋 메시지를 제 1. `git status`와 `git diff HEAD`로 현재 변경 전체를 파악한다. 2. 사용자 변경과 이번 작업 변경을 구분한다. 임의로 되돌리지 않는다. 3. 변경을 독립적으로 빌드/검토 가능한 논리 단위로 나눈다. -4. 각 단위에 커밋 메시지를 제안한다. -5. 실제 `git add` 또는 `git commit`은 사용자가 명시적으로 요청한 경우에만 수행한다. +4. 각 단위에 커밋 메시지를 결정한다. +5. 논리 단위별로 필요한 파일만 `git add`하고 `git commit`을 수행한다. +6. 커밋 후 `git status`로 작업 트리에 남은 변경사항을 확인한다. ## Message Format @@ -27,7 +28,8 @@ description: 현재 변경사항의 커밋 단위와 OnO 커밋 메시지를 제 ## Output -- 추천 커밋 단위 +- 생성한 커밋 단위 - 각 단위에 포함할 파일 - 커밋 메시지 -- 필요한 경우 커밋 순서 +- 커밋 해시 +- 작업 트리에 남은 변경사항 여부 diff --git a/.codex/skills/commit/agents/openai.yaml b/.codex/skills/commit/agents/openai.yaml index 6f971354..505b230c 100644 --- a/.codex/skills/commit/agents/openai.yaml +++ b/.codex/skills/commit/agents/openai.yaml @@ -1,7 +1,7 @@ interface: display_name: "OnO Commit" - short_description: "OnO 커밋 단위 제안" - default_prompt: "Use $commit to suggest commit units." + short_description: "OnO 커밋 생성" + default_prompt: "Use $commit to create git commits." policy: allow_implicit_invocation: true diff --git a/.codex/skills/pr/SKILL.md b/.codex/skills/pr/SKILL.md index 7a3e7934..7e69c2a5 100644 --- a/.codex/skills/pr/SKILL.md +++ b/.codex/skills/pr/SKILL.md @@ -10,7 +10,7 @@ description: OnO 현재 브랜치의 개발 내역을 바탕으로 develop 브 1. 현재 브랜치를 확인한다. 현재 브랜치가 `main` 또는 `develop`이면 PR을 생성하지 말고 작업 브랜치에서 실행해야 한다고 보고한다. 2. base branch는 항상 `develop`으로 고정한다. `main` branch를 base로 하는 PR은 절대 생성하지 않는다. `main` merge와 production 배포는 항상 사용자가 직접 담당한다. 3. `git status`로 미커밋 변경사항을 확인한다. 미커밋 변경사항이 있으면 PR을 생성하지 말고 먼저 커밋 또는 정리가 필요하다고 보고한다. -4. 현재 브랜치가 원격에 push되어 있는지 확인한다. 원격 브랜치가 없거나 미push 커밋이 있으면 PR을 생성할 수 없으므로 사용자에게 push 필요 여부를 확인한다. +4. 현재 브랜치가 원격에 push되어 있는지 확인한다. 원격 브랜치가 없거나 미push 커밋이 있으면 `git push -u origin 현재브랜치`로 직접 push해 원격 브랜치를 만든 뒤 진행한다. 5. `develop..현재브랜치`의 `git log`와 `git diff`를 확인해 변경 범위를 파악한다. 6. GitHub에서 `AI-SIP/OnO_BACKEND`의 open issue를 먼저 조회해 현재 변경사항과 관련된 이슈가 있는지 확인한다. - `mcp__github__.list_issues`를 사용한다. @@ -43,6 +43,7 @@ description: OnO 현재 브랜치의 개발 내역을 바탕으로 develop 브 - 생성된 PR 번호와 URL - base/head branch +- 원격 브랜치 생성 또는 push 수행 여부 - Assignee: `KiSeungMin` - 적용한 label - 연결한 issue와 `close #이슈번호` 포함 여부 diff --git a/.codex/skills/pr/agents/openai.yaml b/.codex/skills/pr/agents/openai.yaml index 04aaf8aa..94807585 100644 --- a/.codex/skills/pr/agents/openai.yaml +++ b/.codex/skills/pr/agents/openai.yaml @@ -1,7 +1,7 @@ interface: display_name: "OnO PR" - short_description: "OnO PR 초안 작성 워크플로우" - default_prompt: "Use $pr to draft this OnO pull request." + short_description: "OnO PR 생성 워크플로우" + default_prompt: "Use $pr to push the branch if needed and create this OnO pull request." policy: allow_implicit_invocation: true diff --git a/AGENTS.md b/AGENTS.md index cdba2456..9e47c17f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -154,7 +154,7 @@ OnO 작업 에이전트 지침 - 현재 브랜치가 `main` 또는 `develop`이면 PR을 생성하지 말고, 작업 브랜치에서 실행해야 한다고 보고한다. - 현재 브랜치 이름과 원격 브랜치 존재 여부를 확인한다. - 작업 트리에 아직 커밋되지 않은 변경사항이 있으면 PR을 생성하지 말고, 먼저 커밋 또는 정리가 필요하다고 보고한다. -- 현재 브랜치가 원격에 push되어 있지 않으면 PR을 생성할 수 없으므로 사용자에게 push 필요 여부를 확인한다. +- 현재 브랜치가 원격에 없거나 미push 커밋이 있으면 `git push -u origin 현재브랜치`로 직접 push해 원격 브랜치를 만든 뒤 PR을 생성한다. - `develop..현재브랜치`의 커밋 로그와 diff를 확인해 PR 제목과 본문을 작성한다. - PR 생성 전에 GitHub에서 open issue를 검색해 현재 변경사항과 관련된 이슈가 있는지 확인한다. - 관련 이슈가 있으면 PR 본문 `연관 이슈` 항목에 `close #이슈번호`를 포함해 merge 시 자동 종료되도록 작성한다. @@ -223,7 +223,10 @@ OnO 작업 에이전트 지침 ### `commit` -- 현재 변경사항에 대해 커밋 단위 및 커밋 메시지를 양식에 맞게 추천한다. +- 현재 변경사항에 대해 커밋 단위 및 커밋 메시지를 양식에 맞게 정리한 뒤 직접 커밋까지 수행한다. +- 커밋 전 `git status`와 `git diff HEAD`로 변경 범위를 확인하고, 사용자 변경과 이번 작업 변경을 구분한다. +- 관련 없는 변경이 섞여 있으면 하나의 커밋에 포함하지 않는다. 필요한 경우 논리 단위별로 `git add` 대상을 나눠 커밋한다. +- 커밋 메시지는 OnO 형식을 따른다. ## 6. 작업 보고 diff --git a/CLAUDE.md b/CLAUDE.md index b53a9ed6..ffaf10d4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,7 @@ ### 자유롭게 해도 되는 것 - 읽기 전용 조사: 파일 읽기, 검색, `git status/diff/log/show`, 영향 범위 분석. - 로컬 검증: 테스트, 빌드, 정적 검사, dev 환경 확인. -- 브랜치 확인과 커밋 메시지 제안. 실제 커밋, push, merge는 사용자가 명시적으로 요청한 경우에만 수행한다. +- 브랜치 확인, 커밋 메시지 정리, `/commit` 요청에 따른 실제 커밋 수행. push는 `/pr`에서 원격 브랜치 생성 또는 미push 커밋 반영이 필요할 때 수행한다. merge는 사용자가 명시적으로 요청한 경우에만 수행한다. ### 실사용자 서비스 — 프로덕션 안전 우선 - **라이브 서비스를 깨지 말 것.** 되돌리기 어렵거나 사용자에게 영향 가는 변경(배포·스키마 변경·데이터 마이그레이션)은 실행 전 영향 범위를 설명하고 확인받는다. diff --git a/src/main/resources/static/images/ScreenShot/Emoji.png b/src/main/resources/static/images/ScreenShot/Emoji.png new file mode 100644 index 0000000000000000000000000000000000000000..b3768b7559061a5ae83f5ecdfbb4f4754bb8a4e6 GIT binary patch literal 129386 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 MBA6osARlNXSZ#Kpvj^f@Avjh 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ǻ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 literal 0 HcmV?d00001 diff --git a/src/main/resources/static/images/ScreenShot/StudyRoomActivity.png b/src/main/resources/static/images/ScreenShot/StudyRoomActivity.png new file mode 100644 index 0000000000000000000000000000000000000000..b096910166bcd4e2b0a9df709babc4d53991b7c6 GIT binary patch literal 85752 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

틀린 문제, 이제
OnO로 끝내세요

학습 레벨
+
+ 스터디룸 챌린지 +
+
+ 스터디룸 공유 피드 +
학습 달력
@@ -672,36 +706,29 @@

함께 공부하면
더 잘 됩니다

친구들과 스터디룸을 만들고, 챌린지와 피드로 서로의 학습을 응원하세요. 혼자보다 훨씬 멀리 갈 수 있습니다.

-

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 xgj_vx$lTcW; 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ƉAv^q-)$8K*@S+#rr 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/static/images/ScreenShot/StudyRoomRanking.png b/src/main/resources/static/images/ScreenShot/StudyRoomRanking.png new file mode 100644 index 0000000000000000000000000000000000000000..5e23ddfc2379a51b352c4d51b60fedb9f908431d GIT binary patch literal 93349 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ĵtIL^Q7V*AGkp^FyeJ}U_*%W;h0g>w)-DG) z2C+Wk))Q=lQ>cmho}(&^^zF;ey@eaY{jfxvogFLD!cJsey`UJ0$S#baH}gN9(>0R3 z4dt1!-1aI!1PK{nITQn9yP~NzYbg07SWtb`*b!Z5b)begrFwA81X^MPN+M&q0*vjr zdUuma5Tc7&ExSH+p5_1W^?qVsuX#czXLC(cOE_FK*Yv8`fWD66{gM-|lE>dOiV_O)N5uS}N}0rM z*&s*<(BWmV0Yd*VX=NhktyXeQB ztr@yhL=RoIp?kp2w-qpOhSsya_r29nJR%p$-Fk^O6XkA32{rzp#23#yn;A&IV9%`E3eC6_t%2heI_TggMDuILrlP1+ zz9$O4rF1J>4VX40Tn(Hsjl-|lq0G(9egk9ZVO*O%+3=-*m&U0c6XuV6dD}CKK1j7@ zP4;KbWT~mwux52jWgRXuRI@QfT2F1UYnvqEV7gb)#{8Vuea2}+%&pesk>*|Esl&s= z1`Kmb7v_9`MK{fR;tb*)Q9V>z*|BI`NW(A-M7{LbaRLrg>59?^<5AubEU*NCIcI)v zYaG&OX}OAXVDa;{T{tVA*>@v{)6Ja^zYabyaNHdsdA`Y*6!xeS`Xi1nxqDj`kmi+a zOcW-rYSpgc(`?9ghkep&qQm0Y_eWchMb%T8^zAr6=YcQ}hkn4p{QAErlHi-j}O3~Maqjm zdw-KbsGdiy`Qt}Ek6%t6gQMP@^HCH=^GUpHQ_(BoLUh@|3U1Bf&jOh1c^ZK;&EXKQ z1ULzxVf^>huJ-}IgvnBQP-II~uS8@1^Ud)55kt z>YkEE)@AXy?25TZ(;BqhD*nAuK*mQOZgLE?nbMYfG9Tb`SSaZ0(k$N6#zX^KBNSPdk|+IE|(}-4r68 z2tLO1?Z_fS)QM)m7I*X1U#m@Qwlv@5kc-VLYj1%*iC#)O*b8Mk9BJ#WHzXwOstCdd z$RMp65Wa#C%*1cU- zbM@IR-Jv@b^+EG7dlkm5Yq!O{A2GYh2Bai3U3_1kQV55o7}fFiRf?<@PbB68 zC-MzH=CMHPBv_E9FJshxA4h`0deChLdl~Nj>0$~Q*Cs3kxmDV0z%?wqXUnf2#-V2X zt8zKMVcPZ6V6RJl5EZ4U&6 z*HnO;!}p!(1``LH7{;<&_&+`k2L_saYIh1d-)sgN1XsltDE^0I7H z+f{8memx|p)y7>PopUe*6c40=KI8WFG_iVQzwde42`mxf0DOzDRfiFjEB_Bm=Ny;k z|Ni~YS}ps^SH`Mk+qR8m+g{dHE!%c2Yc1PYEiKzz{GEO8``;ctE}eS6kK=VbpP)NdR?)63&pwiYTY3RF~%5cHQsSX4$Kfn0)rHTItFEt^vUSMYp~&(1q$UUg1` zP?t0J1#RVspA{EP{xq^ArC4k;3cj)PT`rx+=brbuHKdYWt9^ljL3Z2=Q*vqlBo5O@ z`Fp96D*wG^8Q=K9h#>n{IZ4h$h{DgFx_|0qDnDHBqX>ZMy-Ce`QU^l;d;KPi9xJce zr=t|yZ3DhH`wr8HyJFzz~-XhDHE$ArlQdJE&9W%_5nsEvxz;!^q=5}bC7M#fto_YUXsz?xQZ5( zjmzlUDPf%t54vj$X~M1~i6Ak1o6$Uxt++Oqht^Y)@nK!u6gURhA&&ktfa7OM$%`wH(jdtNp0mUAkkmIPMu@-)S8xQi$7AmsSot7R+&J!4Kb2$8gg@ zCW-L4GM4wkCoQHsxNyRr70W?{94XE*n*9&~wjTkB1S4hGlp3lV&O}m|ldQ}CLB!yu zd54rdDSCa9h)WzKN{TMof|9WvOe0EV#=2uo2>}kxOpxzU(Yhz3ADKO=wO+)6cF%F+s*(FIg^1Yu}9GaE2 z?pzGH6Qz`9ksxL0TVLRveLcy*mg@=`S5p8n7%!bHBW9S9CNee`A{~qp;=So+X7#@> z2C@)AER+q2$0C7>7vM7fNgHmMB~V9D4eQa%Ye9ta^AbUUkQX|9F4pl^oPkrmME_N0 zhd2MIIlYjDJaUG$jgpE+Shb=c;Hg=t>n1RWQ(P@)NYckCY$S?!fOAQ+_(4`rff>fX7I7)zpyxEOK$1=w)k*ql4tr z{Y$=wu=0@2QcNKsZ5}XovGwoI=^%wY4_~JsWo8v-5n~|#HY!GCt!z_hE0!a{;ACtz z`m%zAB;J60G^XfHtJ^NwuCxRSa+pC1jS7XD{kM5eT^;^NGlmXCy9NqK|HaeC75)H`nJ}I$5}YI;vPNW$)66&}?@KX(f2|LuGS^zJ z)m_V7eaa`9|NYzKY_-i?r@}N}3#h?wowjTY0?k1s9UW_E3t%I{<#v?RR^IvsjBr#L zbn#@Q^XgE4Esgu|v;noaN@CvSu{80|OMh!|9WXR?8l3+s8VB_OUFH0el0jQeU~)<> zlVjrfCm<^D1UiA=WyhD2`YXC|;oBpG6;|5(_|pc49_kJUtibRhbd`E#_0;P=IyGWAD{i zfj^=`{I(q3kH!ZBARd41MHC17K0`Ou2!so+|FpXuLmXA$ynNf*+V+|NM%f%Q2~ev5 zs9&5K)jI5d(WjA&%*cL?rES*|D02Qykk!QPAvdarYO!1UG}h<3bD@0WdM^cg&qhl- z%ItGS=VcHFNVOw9D9aLG`}|oq&zIgd9=8^QG|luFYy#ub41iL-E240-q1@^dGb?#N0Rb=>-%}$_Ik= z51vyBN(ITSUf1R(BeSy&dH*r!wN7h=>RBQ?6GV}B8)_`3Z!$mEoX{P}L5+%qqMr|!i;++3OEotC#zg4oVQc`x>T-h*zOne8fuY&^nA&AON$d?dypaX zR?aa^f6$gZ^+cADH4x$RH6|qDbw|41n_w*v$uCmp1~MxjK8#FGa&XF3u@Bt4d3X?@ z7gHWy|9eLml4});*z4H_5LuSRyq=;NZQNt4BbISP?|?phsr`N%%qZ(mBp7{Wgoxl$ z^(AR&cmTsQ>Ivv?zL!6){o4Z>+jcO>(`Im^o1sVhNigKU4>jVc4!F$nlQ@IAqM}&l z(kVOlR#teF7*lr36H6U+s>B0o4ZBL=(_F2*+*v~kp|#JTO#IxU%{}NhU}er&%1)rW=_v?nZ=-M+;FUU?+Pefc%oQKVyL4+3f{?j zc=jQsO!Pmlm0v!iYuKkir9=IOG>drBmx+8#R-hbAFoElaZVQi9|dQAbX=!|CO{X^s^sC{Ae`q z-FX@OYjsyyv(nBLukn4jdATK3-+fuTM!#K2vr3EE5*K#U!-C@&7;ns@#{5v~YL80H z2f<3MV!9W$H(*$0NKKa`J-UU@m>#)G9{cOu!CYJ+viNVJA6`uKJUG3v*HRdfSyha_ zFFDxO3-k&5U9mopdX$xdL_J)}7zmLom(~0xLfxlg`#YRz zxz=}C$M);GegYfi4L9g?j4yIK62)I<28J^Op?bTy8-uX1!g=}r?Ico}BpXX@{{0>v z6a*u7TbQ=a}Fp#_NMw?Q%$XGpiBs*5OMGq`&f;@GIED%38Gb={#c?Sk^(uOA5D^o3AxY_IK>)Z8* zh=?42Px+nc@Y8A#cD89$s}O5m5kG3q)oEh~M{=z^)nI}~;aj>ah0+16^A&e1?WV4m z+zGsHue?#wF3|b1VzHzjOJ*=0d$Eb>H|S9v<(!eV+* z7p$6(_pwv^{(G5ouI`}mPQ7B4cW^sMoG{sJ6+@w~kBx_Cm#d2;4C>kb5gQZN=xC)r z=waL{;14MUvNvEy=Js|u4!LZN?5{6)tW1mg_ zk%8tu2;r`qD$`*N2U}kj`w_*SH&%&Lnbq4PhcL>QjPRsgg{bZ8N7%`+aPx{g0G&1I zeQ`lq5gpqH^1{TyeiXEox?T(9+d3mxCy6miUS(w?ITKr)l`Q#>gKF85$_y+Vi6I>A zn@T1Md65wR{!jecm|h6{$UlItM-eUxIi!5$vT22?xy4M&s8Rggz2&@_cfoa1P9x9= zBD}k~>K7}Bgwy}w`YE`lFi+JaR0NoJ^^It|d-nq*bz zfZcSo1k<|2kT+ZUi}JK7KUqK@3Ai2210CD7@EwN$L&FU#q-|j$6)HBg{Zr?yz>~`0$dyBJ z%Ad}Wz=qKD7bSzbc^+)9A5OJ90Spgql!;4z$uxvRI?_Z&T&a3_bLg=h_&>4!dHd#N zbl@T8(kL-G|3>K1R)3oc6R{-MCaV**pFlZAt4^+-A=5-L)hJEBCB2)*w)i?X#CD*&LNqj~uIdD5(W^d#xNaP*p|eGJDve;q;E6AzY$j#VM++ z9BW*->`g^+8f?te5B{8ur@*&aU0qEjt3CiUAa}P+C|9N(-xoA??9wgD&@?m<(OV{C zVyhe%;3{R|$ILi+-;oal77RDHwzYZX`4Nri$f@_pb;Mt3+!Dxw4|PsYPIhcb2BR9y zGv4u2lY+K!R%wk*O?WoUpFa;#to9SI9g2-2nE3u{S?V?!h!^~C!Y#_g&#wzz)+YP? z{C)lV*N;ksrDWKhQ&4b2jY^gx>JVfj_a)ki+R0QLe5b;d6ufPY>tl)%B(zdA+}}7zfYp_$_0>t_QJPC0+B*po9Tc} z`sFYDx~XZbn1uMMKCyzdBa!qgY!j9(sRxfLm=R*dwet5@j*Ay#BovP?3jN|n;RHT% zKqQ>Zf9E|>@G1%F1AkvQ!H?{Z@7Im1Ki>NNKMIWK4>Io6=5nb&R2%)ZpZg;7b>6EZ z`-4(j9QgTiX0z_#ZKYcDW4HeW87|4=FPSy!fj=wYN|PZn&uoa=l>*l`BqqPp>F`V- zhLS#XruPStB!)oDyTLBY&=`hgZDq9)Xnl)vgso%09}e7lAvW2=0T5}dNd6ommHHJT+$!y^9jfQ@<}d%o8@1Z96Ft|do*q$HEY^h z2I*jaBJ6mLL9nDT%xbN7Go4H=l_7bo(_vvbg%_|s0CQR*rxawM>CVPFYFmE!4zh9* z&QLAor}Y)^h;8g6b%p%{EP=&c6tnp_3V}5)OI90vD3g9a^KPgUm$L0KZw%nN`s?Hg z=(`ZLbI9tE3UeF*9@_4+@LceEE1ib`%c__0chmf~j!%GkNhyO{9w}Lc6`zD~fY~n5 zflm1*(oGAX&M|c0aA1i6?~aJ{QH$ep5sO^(&vyHrUx}SN?hKub>=<+LM+Z^$=0oJ> z!k@=nz6w!WU|^Ewf8?#`sENh_Ebi<6<<3kn-C(qN-oORA>5K&hNESSrd0XW&ILxfA< z28(eUCqQm}o-r^j(F+H4Z8JD$?2=Y}%Q-*Q!0quI+qmh=J2);`_G(SoSmNocAu;u6 zNU|pk7#yc2{cO?ud9i!d?&K;UHgfB&eBM+&l(Vt@QL)tS2{8G6I!aL^6G}7Dzirr^ za1|S6<)jfr7%E^BNDT%*_qKXc_m$|#O9=!A(}@_BHnvR$vN)RbnB5N>d<;fmT7Z2h zm9ADZKtjJZNhb=&2l(%DJY?qhU0=&k;}O{+i5jH)^7GJHQiCN%EUSmgZl%#VRo|?t z=PxOA2(YX)_>H#EnF*7ajf`=4Iwb&jAIXj+(`04J|7XuS|$_xVNZQ)QwTVwVvS+LX?#p^jRBWv5A>VvD}nNGYqNOHuF`FegVm22kB~RrZ!q zgrs%}t_CAnthZ8)O$UwNVMh!sd3J^sR5a@`Gtj^<6B@gg68g{*8lAPCd8s@TOel}gxTWO7RHcO$Axnx{cdcopn{ zj^L*b%qxV>?S)O)3o@GAP#XjB4>)M3y(KzoR{(ksFr0uY8R+cilYJGC$uNAqO5MEv z2Qb}3iC-_m4~~zc2R~w(es|sg@+Ym=lf0PGh0aj+bWXZq+wr` z;++}nca|}|Sp=?5 z3K?UVP#G$Jpn^7*u=F0=__$$U#VSl%0>5IF)bCbvo!O(O9#sEt8?SdQKxPFD6Ejwv zY7kZHyZdF&%g3ifo6e@Lyc}MU{}$x!?F~`)e135HAMJYzm>}c8!SNRkOXl2uP+;Kgtxc1qOng| zEM^F==Wnl5a3mXV?*u4MVDB_x^S^uyFeQMCIKS!mYa4H+{_2IDutWJTy<|!0SE2tt z<{R)i?Z9d&HKKnJ>jyW_7#j2UpzygJz?M*7Va*9pcZ4`KP(-Rt$7>dx^q3p=26is|ySUpw6oZWU~T3ts^jjha0_*RiRweSQx&)Scf z9M<{XYqCBYPwRPhTl&Qy`lu}c*$x@_2jrc$YdX$)13$t-%2enaI!-x&C?^u!jwQU< z|>RDe&1AN`z>P8>WHAKLs=LM{?g{?6uy9flgKAdpvQ{66vz zrf))x3e^f+*?*xd!J@+yc1>sYAn$3-#5-Ux{(el;^QVHhuIhxR)9Fgsmiv+yARtE} zFPw9b@fyT%hQ=a_+qNR6)y(YY1`urkq&TOYVf2PmV0-w%cMCK3S0v`=3jhT`{4mJ* zGm!Y@KoSt2)4u{BE6-nDJovT|R8*AMLYarfVdx-UAJc|ZuYp;*I zV$`Y8;sy-+E*d?QT#N05E(i<&%`u6qmn`Dc51DJh3$3(0PHL$qyca?{KCa7+CK72Z z$^`l~GWlY8P|i$LKTw>SDJdV78PY4I>?>T=|J#N*!E-@fOVzZV&& z+FB@-AvfTdghM@hqhiRt^BK`0&@$qpe_c@7K05lpncy1D`kx&CMb9JarJ z^2)dX^*2Mnhj!Vq@qz-B*;$_oSKJy4xW3WPR9LdzIgW8$KSupHmfY1TS^CuL&z(D7 z=k{0V`grO0^QS1*3T1i?Z$O^pmrvyOL@mSTta#$!4^zAN`l*^9SvmUqeIy?PO&3yT z#raw84nLVu9_EtDz3xeB?B{_->O!b!8IDXSB5w7!$bx1)*Vf1PI^`3H5%vF+v9vAPe6Fh@$L z>HgB6{CM1D*!P9~e)CZ~z`AN^M(9JPV1NK%Xp!rcf3Gjel+JNq@WjnU@v0oOzDxzY zy0Fd%E9qGo&C*gp=yeuJTlRfeStUL67Cpv6Eqkwf^jz!BbI=#ulP==RO*6mKTiHHU z#un)Otz>KP1Sp#UOp1Clojn9Qdo`zA-|)O-$M=tH7XbZEqb{%+&{KX1e5F&(Wd6kS z93%0JJ89Hqq5z$GO4B>xA`nSuL9|{Gpe7mxp9bnZKy{B6eDMGtYMRDTE$Jg1G-00= zbNlb~b)60_?49Odlr{7s&vM{u{`j&-MMV{2=O9ywvg0E45?^H6Op0%c-8hv!fI|z( zd}5`0y&i|->Iz?Js^~h5=CMIAaly}rS9;z4T_aR66J%yhnGXKfnZu)_EM&vm`4>)g zg0OD^$GRIVml8kCwKMc4u}b@bGtC2fi~cS>S{#~Reni-*bnC`*(V#Vwi4(nRDY?=x z5uVl2)A6R1>Bq2$QSVB`+O?JI@t-3Se;*>Vtqs8E4@5D+y)Xcbt|pD-^b`Kgu6)dB zuS#3#7rnzhn=6X^G}w~JHMy6{5B-0>Ql{e=`h1w`enzY5c7~~_G>^Ox|DG66zNn5v z6MW&#s-&V9o87S?La`e2;|e1r*!{*e+-YRANsB9hxs#|C-Z{J|d=Jg3 zZvL0OOO#>&Yix`C&|+d-l+UF~WNxx?qt=-c1ISXNF_QjXLbLKVOX()PO0 z6S#fe_S~J2ybM3@!d|%ti>$;m$W_z!l_%b|`py-nvD@HVTPu*0&qYfO$N9m=ZQf<3 zL}8`J0d5}2U&K8SE0>EU)$XA?Lfy+V-VMgjEZ@#l8A*Iblk0e=C~h1{9%T;8*IMq! zW}|)ZZAw>-s`6$;NVmEA1OBB3NFVGK|Mh!^IG$qLS0-?M1@1Lfk=E zd-;V81d@v2fdx&vp+d#3D8tvsAtmvze%%D~C#@f8;q#tMLYQZ+yPv8m0bEJ|UT9cTv%pqE$@ z^$Gh}HPOIgya_+tKZap>ti%t^K(fS@j<4j*+HKufPsN>_2TK0xh3!&8dCTgkT3q#> zInhdxKLC$!5^0Wk|N2GVAy4^_aLP~9Knycas%9p#i1x`BGC5UdN&I6+>L3e4FCZA; zKAxi)=!Lc5d6A!b*+tCv?Qm|-Kj=(6@~{BKZ`u>rN+N9T zO2sfx8W`+R1j$|e!}ST$cUC?TaXS;)-YQfAS8|gvx7eCWpVa-#$25F;j8j(i!O%2x zqhELX-LVwTIfet0xCCb^9=#CNZ&M2kREnv7M&Ai%eHAc3 z$&=Cv6{S!4j&fN}HJ0QiJH#tQ{Cx>_z=iIbuY#I(booLdBY~IrI(kt`?i^qVZO75)C)8>-g(qq~A6jbKwqt8z`+HEYb8`#*J$aLzpWcA2sr1i9`P8%E z$)66z)lii2EOwl8|NZ3F%aY`>8rpsb;#FE4Q#eYO4a9KXYr5#^<6)8V{!s-FLXKM@ zRX;FMcZ04TGd+LlPUmfp-L(Ye z)>EQ!G|i)}*)J3tg4Mv)gL!XCbEWZBX3|zwO?2QA3G;U{K1!i{(1fioFqe&oIIxLS z=sl`-Qt9_$?bRNHvu`|^1f|E)f!h?mA@WqOn#N-i+}~Q#yFW%m-8gm`%l=s*13FvR zp&e{@dg^JjLYf5HmGV{4OT}Ywb!#D~=sg%8l_dKZjaO0Z4ED)1V80a#yhP8qh96ix z9u;x%$ZlKKDmc~4!@Ib0Y;?{NJa2g*cRG^Ol*R55Ib4)3O66WC}u zw~tst*!|P!b%Tc4g1MG zu%+Pm*To~Lpq4Wt_hrjzJWDyWWKhAglhjmp+v-kK<&5|Ye2}R2 zXAZwrmk?T>L!G89ET_XUW<^R_HEV@%K=;or&n|w5TM4E_ERD`P6V~=(_`%}GM8oct z?#Skmo}(=$HjfSY`fr|TPs6-VGy5}t9TQnKJD2z0eM^Bh3~^QLG;(L2!epZ~5U3hA zk*G#&P?S66J7uZmB~)Q$_2{Uud*;f?kP(Z9ItW0s>r>S+{Qlk#&0G zKPPiXK=;Az`Z%5(C_MBp9E{L}>#r6W!3U8CeDd{jJFu(}H5%Z1q?E_Pd3@xS)M<|> zfTxxVZ&)V1V+We@GyvHsC3DW|1sqD_cn0AO{zM=a?>H+PKw=6or>+;ab+V zMlsgt+EiHBfY{MOC|ooaTb?`z2m&L7xPsPW>G*!Ihe#T%M%MaO!D>GDZx{iB+#(BP&?`w#ux~s*^eFQkubXht>ezNZ)T7$X)k&EV$s`GbLsMEk(0T!6Drz* zabSKHAt~yCJIkJ{zd(D7&0S3&D;b@7HNJ|@9ZEoZx6iC9g;O?+{?)sH z<#Mv^hO78!#+ov+uMAC2Hd#77?FI$lMf<{B2vEy_GIN$7RU?ygnXuWUE!frz(atZ1 z`pRh!p1|9?rSb5~w0xqa<;KTBDFm?6WMnb~h+$(L)c<@yb+xJLZGEux9x8`8d0AS^+p z=ZtM_bL@DtWCa%Rh8o*fH*&DxeSSqNnaBxmUR#-&o59V7UfK`rsKsmgoH=a}fW$te znDR*$F1e11NZMitIfqPX^=lgQf z!;eNWT-kDix@%F_P6@f3Zg2q{^Z@^nki9($;N$!Y`faTqh>DfGJUjtJ8!R^^L_3H- z>vkw99rd4;>`(uh`!=EO5xsjD2DJPV+T|W1qqvEeA89-`f36w!F1Cl){d`<=g5Qmp-^uOry$ zamLOECh>bG5#BU#nptL~`Ltv0maMsa<7g-?=E%6PF%57Jo3j#FQ4wW0z8)VAjTdDn zb0avMlhHd75m{!LJi^zbz8+K%#i#sTfKf_`#_Cd&vFH)T;W!og}broNNX zZKuHb9Ehfu-gaTSLqbk2yJ#`;njOJq>v?qWmdCmjCO(ikR4ib*1gS^LmLe$%l*iL9p%^E3Xz&%j@=6 z8!k$U>Gbo9tGa^YimR0Cn-b`j!?T+G5XkZf9@Lag1eF&|ve# z@DQBU_4!YGz1`7wlXKM^#0Z(YJfB!5~m}Yv#ufte)KVu~h zwj^yc^d`>! z>c$!SQZqog3#iKlG^rusI?cua@8z+y2H#v4JsJ>Gqz`F%s`l4JxAam+F?L=Ap^GC# zr59;8bGsgVuOH-aScd?!OXId37E?LG=koYbTLg+wQ>&=pfKv28BGlH_8u6P=f!5!rnn!$pi4Fz znH4wR`e@@G)BcACy|>TvwwRvF{ibzwg_NzOIivjcL65&&nJR`!Wyi8fpZONmvEmqx zi0=ZK?RGx)Ne+ij?^R+g*t34m0GJqxYDzv2c>>G)3hfiV3H1SbzrrpKgzG#nY{0U4 zXlC*B0MVVCv*`KH+9% zcDX}xn&#U1N7oimL{>1BhK z&V;@gaJ{;mSbM_D$};x-p*iO%^}@lEdCF=pWPEZE7$jEK&@f+|;g_ke&$CJD6`hIp%f-qqj83TM`{@8| zlM59s;UjSE14ti-#V8a%Ia;=qHEaDkHQtm@N0u|B5M2EGVs36;keBx#kh&8GY_}+2 zE+#ZdQ*bh1t{h{`JZV|7Rg315AI z=iq2;Zrr>%{M6Xg48-#jvt<_sdgR)rz6_WsZ0eXs+T0dncK7y${|qO$b1-ol*#$(u z7GbIoDwm|i3`4qhKl$3PvJ>ZW`ipMrEhc8mF&A8{jtoprWo$NK(nQ{u4}NeVPoK65 z)_PEG-0r@5Evr-ITT&=ffa((xFgjl!j|X3dyREEdFp65!roaWqmfnRg3=4o)bp_5+ zU47vow7w9AOCQtEfFHK?8RhQG~)44U9N{J4LL(~ zUwfeBvUwIFIK<-%tjx+)q-fGIJPYNt#BLO#+&mxWt;oYV&pJB1zo#b~RrL%$QjKHS z%zU}GbCLamO_yaMB8`YMF+@Cm5^Va&RIN%;c*9?(rKIY|$g6I^){=XD0DRViSH?jIK2ad98UpwzG8ju> zq8@OLe0tB9@8|fSXd3#tshr0xqmS#4)BPPghqgwAN^bZjTl>+#PYcwI7!mE7*2V^e z&h6~z&sw)p$bCVB7P)?lT0a{7y=*}Pec`CNx9fn~$M#R-NUNqgI5`rI@%So=<)Kqx zXK%NlxCsXKj+n(sqFt$)<_3kB$C{5e8k_!C4WSQ{$Lq<#Q+_{7q>IbxMP#PSZPH48 zo+<0Pz)MK0km0Yh!PNL|1~DfMc7!gh;X6?8{xCMP8yW6pAy98m$>CwUna?YJN|G{N zia60(p1OJ7z2IVzVDIx&k)+Md-kfCj78Y(aB_B&i{S626aVzv2UJ0lLK=0u*r*qi- z`L-~`RcOXN*n0~R)zHwuAB%?S!v%Dsg$h4M#y_Q9b~qwCis_Xx%IMWgt+sQ%k~&`4 z<+VOnRdId6&dAMm5ZJ&E>fM*C%cmT119*Xh>fonrfQUT|INrs=QaSfwc}VUL0Y|jl zetl69uEP44Sj*o6xJc4ToBR&J_g0h~9>05b zFTTe2OVK3g9uo~+HfL+Gi&u>p`AcZ*M-o0LZ03FYUqCAUOmdyBkqEE5&~h+VvC$si zJXGNA`C6;RKHJL=(4I(qm&pFRH36+*Kw7~d5@1`K<|>?5O_LGN<07rbv^~gv^>EN^ z)=WpQKPU!hjwU=z3ebQ{Wx{03n`%!>L@GMIe$cZ=EeRghu_@0!jnm2A|c_#GAazqivHKFxCR7G0!=IG z6GxDM7v!|E<|w`TT_Znwga=$fy14o;r#;n$dzRhjX;rb1#{u1QcXC)avX95t6j-?9lL=&7(9mn0) z)OWphE*XU1;uA2XnL^-X;7H-~xiaF}xcAd@Cb`v6&i(ioN=w^I+Dm!H<<#$AhzZqi z%UzG=xu@LD9;!NEBA~yG@?gh|hAj+h>mb^8s@L!^)VA$317O9pc5$6@R~TkqccAMw zXwCLy>Dtkz51}cECOI!P1XGXd^?_fVZELkVWG1Kjxtz9PI$Umy6ARKh)gtI|n(b{@ zJ5gpV$NP!d`PoAeOg=#b|5ycc_lwg&Y3_u7T_d+x09F?+pSvn6k?-(~P%JR2%2&CoahgPb zn%tVSf9o1^P1^8yZyeti;k$uwNbjp<2I%)`X(8NSJ0@ZO6Wqv-|JnypFEzQUVNHbB zG^b;37$A6?^hJ4_#176PDtwxceg+{wPlns0b83L?vrnBFWcT6 ztq|%-zh$L&6W@qSTK18%XKgMQ3%Q=a1~cN^J{D9cIi&EK-1?eKTzQY&wcHt@!-iI& zS?x#6${D-7Qi;NBmebA51D@R1QfS+K3NS}**@qJ1rcTfL~i?3nhk*XB<{IRkAJOK$$x znV@aHx=7RB8&!qLAi;^@d~~J`s|cgqeAW1ES2Dcdj|_4g*87?4+Dw4LrmL0Wzllii ziFsL<&R7Y60Oo94qS@dlr)o|<&DH^xEJuMJ3;WjvJ5wVwRHUa2@ntn@fs6{9+=Gqn z53m7F2(!aZS*FZ_i(qC&iYqM3>-6=ahopGftzq+y4ZLOdI~M8gqXE#n5tB4mR)PR{~$y8$yEpk7x~93nEGWgioc}E~qHp zX61f*fBB94e38OpR!B()^2P$UK1?>f97acM?;ao8_zqmt8 zdeoi(WDsaKLr56XFM0qHwj~yzFVF&ru*R*X5URs%kNn2aFMHw$}?K8agUL%3Oi>YvS$u!qlGODq3S(AZZf8xHCfd3$y7eGmy%C3 z%hUs1Yf~VWUdv}Fl|&cL?TIxxkVdW1Z6Am^uQkm+= z7xC2;SM&f=*Kn;8_)%Dg1{L;P3aSYGNZUv!SHW; zey@dyO)BGF4F<0RiOe$txWJc5O7AG+Ezb#a>5_85QBH?@T6%7k-l2^O6%p2gU`CRUnDZOELZ!$GMwxE_g_?1y z!#TZO+K7x~o-sWdS@xH6*T&^m;Jco%s$YJgIIhn#h9pK74kXjb0*A%yQBLv!VWL>) z`v|8gS-vce;{TGh%ap9X$zFnlG0X-g49EH2FtRG|cMl$|=yM0;0s&@ zk2v!q!~`|revYCv7brPX1HRNUdFFw9(oI>-yv?wfacbN8JV$JetmXYXs~XR!MNvRk zf@)c>DpW`q-h?3tH*N?$Mgkore4?5%)|wl*_l%>UgntBrT?(a`0=W*hejMm@V#oDg zLoQ_sq=dmlL>xiGZqN0AK5haFB>-)`;yuSbj^w>zT#ri@4+j*N7HK+o|{b`O>GVtW^P}s?-UYiP| zM+AK+D3oG z=izT?nA=ueQNdEu`EFhvZ3@q?C@3IP0~p23fABL$#7sN|5`Xt{Eu$0_5C7)~bo`z2 zfsao88wbHuTRXjN7k~#^ZfPmOye>|H@$p%D43A;oqoP+IWx?rLef?RH!xm2Cy6Wi% z2*E*8My7)_^Z<(xxJUe58t+6!ARn?-ky8HvV`1+kIhQ)|t@gb*lq>sD)|w{;D{!0OK-tC1pX28k96JyydqzS%wIp5h>q{@L*wBKnO9_U!YU1 z*GpYWQZ4EX0E5MQWpwsAlit?kEjODv#e`kd5Hu)4`#8mGlyzbq#H+bh1{?{81SyKP z{{FAuVIu2q8-kJD33RP^{?>8tzK-*`(v=o|NgkBX_k2Pq`EQyQTb;9_fciRFj3v;M zj7%0uS{JI7)?kEUOFFg%gP<{Vv$1W{wUs6{4dv#_OkwBydG+R%4sHYGW?2&eW$>lQ zfso^)jtSw)9&R?lEsR+7C!muLqo7Nc+8rer#Vn_AVfW%1pp9^b3LFC5#ZfsaC6I|Q z9->JrQTZ3wmBB4x7hXaHJ9Bb7f_L8A5XFE_bObe}liz3E(pK>;QWO43w<{T3x1aBM zHl-f#&NbHThmnk zaokZ3SVN2}ENg*_17tQxaW$?3CdY$i@(?uyI6?)~UT}!_*9&GNr!wQ4S!mgt9wG_a zne@dOl=>^j@a-f^C~;?|?o}RIju=_PjHS zbRbq~U)b(Ud*8zU3=%eiF&XO$xo2BzuV%-mdzxrLKB~4<^gHxc;A44-j91 z1BHH775>TwOp9-af-j%n&4sK}%p1bZ-2g2dm|HPHd^3#YautvsEepZGP zyiT7=+t*2<9H=cQfC8e0Zy}G5-mIm&K=zJ_iRr)h-<0oK2UiuQ5_rHoqC=2+jXe6c zZz4O}AS4Q|)hR$9n2ObgX*Fh3iqV%Wmh!9uk8`yN7Z0xo5GIL(xt;rM_y1k*$NZX^ zEwp6R{sEo)IA1#ca%0P=Fm@a3(b?B3O9KIo4-p6Wp4=Wn#2PGW6@l&5A7nqm7_05qry)yRIjbX(RoQa+F>MTqY{G+}kb@BmmQ+9FHh2y$ zlA8o%a@dKuxLlhdd(wmf!^tKEX$+28Te|?}iLimhOE#4TSg$3u$;zOemUNofc7CrX z-943gvE#OAfH#Nwcih^wI4)`=mw{_!Sf(#V9!rKUR_gcI5s0G3sb7JDDpHSr^GQdU z?p*t*oI|zc|7bePs5aVe4OgJhQanh};_mK+;%>z?xF@(3iaQi{C{Um{DegsrySqzq zDDG!?zjgjkRx-(xnR)iU_I1nSRm?=7`Xps3nrc?$g34^#sfDAToq z?__qSa!`$fVurq~#rJW?ms<{yIci@4=vPk{j_>i6&j?X32nZ4M#`Hax@(p~(O@Xk_ zh%n;usoRyu1z$q!&rYK=la{}uatBrTUt65Y4s_P@Iuog2N~fm;!Ix!ie>J>bAXu^Q zIyF6JKCr*D>i3sF<7D&o$NT4C@QnL16| zvv~BD(KV3_gHXJ!xwS*rhWb8(9wea?Xn51F3AtKw+j!=1qV|DBc&>EXa{Lt?sJ#dP z0N0?alTf^U3dF>YqpSdK1|4qvcuvgCNwsE8w>*h3bwEwV9isT)A*k4^&eHnXahxbm~2}WQRkTO`(WbkGVAnU-9$57 z=WJjs+EpIG?O@iACd<78*%-L#RkrX#_w`11Bh`yNyMCJj@mJ2K?J@=7sdZy~@8XQ8svJ0Nts5r#mFhIq&C z8~r(xC4$i9C2rqF%FZ--Dqe>)PrLHZp89vc!@#T<9cN8hJ@Dy>6E7j7Wm#I>cA>iE zfS{-$2rsdw`_rT-C<*~12EKZcOu!IrM)WyWIDT=%DKWB<#xbCx@Ge2Spvm>H1F#(` zVsn@#(K~3n8jH4Xo@w2W^gJFNKMre!Ei%6&#}hUe3c?B+NYcWzlF$9HfL6!OPaGKYYL;D7RiW8NiQ4Vv+@ue)gE zAJ(3+TZ(VhAd%a#81O=^9k0hf(>=lhvockDU@qA28Hg+Ro(=5=>imQ`RFQ1s)~4MI z99lRMbl!`u=z9@Kkq<+sKkPOsqn%q!yQ8+_rt+36s&SqEWqe+0OW4U2)gKeHE%S{y zO&D_Hp-(;Q=pCF`lNMPdm%ofuPfvh2a9_d5Y)jeh@d%wNDU`eHJbfhHk8dgAA+%uE)5Sks|h2&i?Q#?fr)UimO`D3{^M^F}M zGHYa_M&L~WwlvVpS32Q`9CR5-Ba)0t_hmU5f*2d;^p|!9T+@$6oml}rCrcr10Rr|7 zuP>IpSjo%7;t&^%Zgy{cE2?_C2qW~i)M^QK-?Z$PH$k2$ce0-n(ri_M_T|PKhhfe4vjGXP5eMqEEyhZZYym=;>cJtL}zwZ|{Swm~N6m)ti(g)oA!h z^9SMuT&^*(R!y7_PB$vl>lIM@DgjYgDRr=SlRruWxh%SkE>D&(M&w`__`>;jY zD|}C8RJsH4F~L5yX4$_nU&Qd37vnZD6qs_Q%_xokO=ngeP)KWyJ{4yMlCQ6xbKyug zNb0&yAMcKPZZGx$W3d_9CmDf>x}H#Sx$au-#Sq=J6`G_UG#7tUw&yUgy8~CQqqT$ zkkiD9N0i5r6`{zd8@OS}3=bvJpygrqUu|BZ&OSA~v1RVSA)DmX!ScsP16%Dn3=wZY zG&+eS;-@^rAUzMnGq&L3CyLfh&@6C7f?)(~DEU|*k?wbG=lLwDy0}-m%F-f~fc&u_ zo@#cKV8}%S4S0-8-w+ZwUA%Fiz=(y(Fv1T}j}dxEgB^L1`Rt0={JEw>;5r8=)j2TJ zAi=Aljm6Ub3|z@+rZ<2DQ2$a|kFoU(Z)9V8!Odu9KLj2$A2d%-n)L`Df#>hRk9b`3 zO6pL_pmn$RZI_=8H$&lT@Wv-N4GX=uf92xev>A zKNz6~rGwTUOKGs;k99)m87J^TM(hgaK{)`p4|xZ2ODTW1u}yCEpmrKHorM(MWLZa3_${JRg!TC#$Sh^WHvr zK~Mj5HM|GQ9lG;U@TN-tdRxR7`^!4}Qq3Kg_eHvhxP%!d$?fVPEt6YbRRucN!19~q z9vXo3Pw{A(+w>bWh7L0QdVSE&EDMplN74<$Oe4<3kCImD%ViKu% zUg#MeU)K(Q^I%)rIl#BM(iJ{P6%fX6k(`QiR#<4Of$}R?S=F{|I;6NCZvtN>7aPfY zBx-0b(I>4(Nq*AjP7Vj-ZkNMTLSrtTs#g6*MR-G(?tljd;WK9S?4#f^rx#Bb(@iH` zyV%Hc9a^yYXr3024}w@#DDOj{2VIDzhIQ&-ch}PIDU|5tRfRtEukd;<35%fD5{YO2==g*bIf)v!0%5-?D$A*mvrvlUcDjQ;qxIHsZ7lpH4 zCq$l&O_j%gtkgA=jg6f+md}Ropr;)Szi4CDcJZ1{2a+k=d+tQO`6<5$Xdz?NuQrY2 zc65aaVguXMR6A9Z;l!Zf&>Yv6v;;oJNV=E(%|4F&2cg|y{L7~AT8l64=D0C?5EjW` z*Ou@GJ4pTE^=1!=xS$nBK5e>>x7K8DBwnpI0>z83q+#^2qO47KQ}KkWCx35?|db`a#g zM#n!pY!Ex)s05fv^V=fp1jk<&rXq zNK%R<-O)@73VQ5nL^pLy!aM1xz@RjUvx)?f9ZkrkJG#&5 zqA<~sorIyFByE=XhVt`{5;#fYS?! zni|M##ORU`a@&ZAu|>K#dD5PL-m`o^u5M62LPal?8LG=Jz(w4sk-krO8l>d=@-jzO z@cx)(74HsheG(#Y`9Xe1b=|=V*QO|XrqBSmui-_P1mLT|Av#B&Cq)A?SMKN8~#!7!memz zqGYnQ-51nidivAz2qm(#&<>SNv7$Eg*E^prg~t5;VTs6w3$Ds(G8y8U#ceUhtUrWz=<92RzQKhC z-l!=3;U{=XBGF)9%;mNV>VyF&{Tc;#(;a5c2)NdK{R<#(gIia_IFefIgg*ivmsutG ztWU@=QY@5iq{`~ikd75cFlb7YHu=m;(0L}1-4i}Er1iTLSFXQ!t3eU7jF-Ic1%B3f z*f0AQ8BQkkMw(l^pO39TJYGqg!|#{1oEZp##9n+v(7pkPx$78`qX6PFsDlc zt)l`bi5RTHr&<939>e1*26t8F&7FQs>YA9(RqheYF|gtjwa3OwjaF7q|DAb@8VQm&BB-q!7bictM+LM~_a)1vj;QaNnM18ddeJ>$a zz;`Tua(2i~BN$B%lo)AmhB8=+P3|a?wW8OwXt}guOmyXw26KZM4kabtJ?L=E zn2iWJ`k=%1`l$o>wFww$Yu22^{+;4`9?Fubcy?NgXd?GV>KoP<${+@D&yw=olB0XFZKk@_*Js46!Kq9C@ugt?eN-NLM&U&?DdYKlaWZa{{EGawGKLMIZ*uCnlX{E zH$ISg!09BcdM>p|zBUUp*`D4xGX{vF6Ze8tFWt-7aCsPngK+(QBOT|0l zXGs~(w7{L#V5RM!q~oylmU?t)&Lj&jOtRAb6r8#8;!GT0yHSN2)?JT0AZ_|d{I?z& zmOyw{+(x^4VD33hq29Y(s^eydHA}*}u|#j3?k`i~UsY01ut8+yC)@@qU5;iS7lR5% zyonuRuXOcV`sgr2A*%-7NI@o+W|y>UuE5Lk1oGrG(h2Gr_fX#pkpg zIX6>;J!4**oIPYG09{A0Z``E@oPj1XkM}zwJm9|La`3M~yuWn2fB$xE+oi9v?$i&J zdTrDQb(2@mt@AqVF}^`U8e5nM2`=F0Qh}qPaw>1M6g5C{1^F@njHd%NK5w_oZ45@0XUo z7F?aBaoG%dBvuz>kK6RJZGVxnQ3UB7+DJP5T0+L#f0r50d68wUM^A>NNl5}q?n3Tw zsln1@9AR$d!-@)m{5bpYUc50f->bkOJGFLCc?M-0ffftO;;3n4wxs~Z6+fh! z%X+lZwxXp95(`HvlWim@+nE8H&pX;0R9B)ZDzQ*qRd;D+JudH-3%6lbVb7BfO;SF;PTSr}cp4eK0Kz@Gw{VKquDnevozH_ho6di)g^gVdWL~_ z_C_!`??LiD_w*O?HVdrq&`Qlml`+YR*>0Pb+&RY8)in(}c$!>-WofSpfn8s{o>?FC z96Sf)4m$5mlVh7ROEa?4iTwqkKtr@bc|>UaL@Pq`x`d+-;!6Amn1t+6;;o+{s7p1< zY@b^zif@a-KzAGy*tY`2U-H~AP{bNq{O1zM#J>HD)`@^mUsqQrD#%h$0nF9C&gPU| zCxuRm&A?`_SN!$sIvc~k2)th!%&eN7J6EC~Mw&BThiT{)X*t_B;RTRK2dsUQ89n9w zs&-`3FFzwK_)UxbRa@12pw9=BS1XVB;83WgQo`WCc6?W*_7u2m)HBPe6REpgwMn66 z=rv<$@{i#&><6LC&TwXTqqUV)J3yj1DGulGww!dJ{3fqhNLJqfsbWySz-#z^4 zXZiyXfh;We?f8;kA*oKAyaCldkCttpT@w=!5S(nrE2WucYz-u*y8tGvdB0CDDq1e* zQz|EhbO2X4OwvrTv$LtOmVhcB4w2FF$7?%0%*681;z^76o`7P9G(XS3Dfrg740(O5 zvj+yA`Zva=G!o;yeYp7zBxPRgsZWLf?&@O(veCy$s#m*QVjJJDi5KeTCeZNX3p&`5 zyCg6=vE<&T&t@wHmP~#@iRt9of6cnoHbhmTvTMWIAw66AqxVaA;+F*)9QdH!+6w4x zBQ?+sDv=9+j~F7xK36-z?{x~tiwbJ+INRQ(T9S@@y~Ty4bpE2G?xn~b?&!?h<_GLQ z^Mv1)hX)pa1upb4>M~R%Wb4y0Q@k^#=j*p`otZAv?P-aQIVJz*?~{LANE;MAaM*FW zK?C#I&;j8k|ET6QHNS%isP=rN1+`aZNa@zv#eHv;W9;#1fDp;R@e-kfXWCA71(E(s zP>R*apCy z$>owez1o44(Q`tMXej;D7DI~pDatQWKblaDj~3_t8twN4iq`UCQHfFg<7nXq+;tQZ zu}S1tnDCxr3+I8v-Gov}op4YW3eC4++|!2);|-l38_jx0+^@eVXmhBge*81C()zUt z^OKI>VClkdvjMFR)G7^aMO6(A?qTA=J2)IpAKt$x6eYMtZ)ytZy08HyCt z;}cu2C`7`ORG}xV(Nk}IAsg7`Y1%6_5`FsD`JZ$j2|7u3705v9t?JE>^*oDUO?K;T z=S@G8BkqxUJdcb7oGQ3al#!9s`@N@4v30aT+rbeaHd( z;DzI2e@PLt4ts^IkrLKD-wiliQ`_hgK>Y7ea=0wb3U$mdBbN@Bnr0{u1uth!OaQ2h zE2T8P>aQ|BUYX1kQ_`2dH-{L87NnJsn$mb)DHSV?h}aK>i{(pAZZSBQRo!u7_U$My z!m0Nem{4Z@@5F*sQuj~rfh%_x0K~FPu~Fj>yMgEa&p;miw7t82Bhm{K#=6g$7)^?bIq|!+h)vksXz0$*~(vIiq`Zai)o9%OA#Y`odcI{sYzP(8K{wWfwEPrEGTmD8P`s_|ghzKtDk)!^FS zYA=;7UCtc1EM`h6hr+f=YChgBn|SL?sky6;#Sf0UpE;9qwkBd(hip1kpc7jvf@ws* zTo6`X^4DH2o8{lHs=}s!wx}LT?byZ`ah&bjW+Uzr=h`4gIc23Lj8Z;MCui& zMr5;Sqoh5G`F`M^QH@^4+ubEUT08TwU+@)@ZL@B17Li zyuKm(T$8mUMR*|q*nxQaYyYq>)Lbl{Y1gTO-n)d+dKMBNR~LjafZi4BQB>tU+u*$0 zIf*d`*r@9=hG@vH%5~D!)P!Z&oI)99gz~~L$7w!ipKSb-=LGnh1}ADPVHul<$R!w;om>{(4`1YOj$iP%;~{Z?sjxx6rBRHR!QsGoUI>GPkuHDrcI%Q&ma0 zd2bS3RKGFBN@!u>ouAVw+#NJsQEvo}Ea&z3n!4epa%Ti&SG-)k7k;wG<{3`B6-d zy5vh@PhSou%Ti%Dq8)7=oQqY4E>-3{nNnbXj=XGlt~v|O>`6B?CMG| zkx6cKQ}cxqI8Spt8%A^5h;p@X@}9-;@OSZmrV@k#*P%o-V^gGz21zCl1R1ef`cS^3 z^d(9=(_r8mG6JF#Q;BIqoztA z?1uw@6mYnuyYhQ0X9$@ZCfpmZzqtgo8tF6FZ4 zG{sbo)IT_?3w3?8v;5s*BowAtY_z&9x;2TR*>*EZZ)f7LSowfK5%40U*DHa_IOehZ6Xv^e5WY3 zf)&LC*mLHUd?v#bgQB{yCNe04Rgv0yH%5EOq4e(5od{Sd{pUHo+DrDc!5mBYsnRb{ z#ylBmjWeV{Wf!1Nm&Ycbq3_59R<$C!KRll)8Vy-3b(n>(`a9EGkQ%mjj82a`PA_%% zZuxj@EN0b-Y_JHHFXOVNC;-rUJ$aFA|?GukZQXTHGRPX@1 zxc_%pgXieA5eIa*MOuj%6TsR3+ z47&LfH6t0;)U==#t*7~GU8}N>CyDkS(rQ z5|{s%#djaH<;ZDdIXGG)`wni+NE;uaNm7l=RsQ_CCrGJz6;AX*koYo+$t@0|4XzML z%ri+N`|=Mg&BT=zKZA8TC1z7!V^3UQXzy!3!*gu%jXYM-?j=@V;0;5~AR6~$)tqtN z+>jxMCkv)zlI>_FKjuveOo!Da5+uoN4Ojr#4KeA&1u;L%<5F%&&zJjO!0W0%cmx8m z9TRJDUK4K2zx4u+)R~(NkwNOj6hSH+z=BSVQO3v%3;nBc5a;J!(;O#b5>KA?o+2x< z!wOLRbvQFhQ?HrIHIBV{`Vhw_0OZm3dy^+26RLD^MUOzM6~ddi3D8q@lRE>7!U0Mr zdTm-2*`z+r5Ljy-^7065X`u#ADua#0r>x~!sc9nr12E~rOb4|hq^ZB7cZvVl=$p$v z7g}Ne0$hku$f7g>o=&H|9_$kpA=3r7HpLp%D==b|EOGLGe(OJ45qEM_~)#qRCyE}On5A&ov_!N2Du~g-m2c<&CETsCEC|(SIu?+yKXBfEi#Wp%XCq!Jh zi3BKR4bYJxPTXwI$8u_ZE{e$6)XFItnmn47lyjeDf6KA=hV9F2V6s}h^5owVi?LF! zw|~{sNKS+dyiX-K`py8){NNq%vpP_gdwF!54w~7-{$h;=d!qIG;mZVKgh!=-tjq)9 zm|U9nwPUz4fZf49gpfG|-M}K&D%G?uDtsF-aLrCk1Qcfb5Ij#n0h4i&^-%s~90rZm zAqO80jS2(Qp+5vvAIFR!7EIdd{AJ3BaD>j~lkppyrfZ;tpdAnz*1GU@RGyxgRk zst@m&YKPDO*uWUJwJ0wMtN*&=?W&u`kI64VU<~Na zwWPR(??Rj;GM(vj6c^;*rVJ++*9Xy`iPwPe>iGIO3kqOE7PS(bV_Ja`4flnG<>05Q zr^jro#w=9fy3qnZ;8NM3F{m36TmH?RJUmCb0+YLN$%OG!(~?m8l$do{=QdGO#*a^u z>tyF4jd1IO&qYn|4BTLXf&w#ypo;9r^FsHvAiR^<7;l8^hXV_RLQk>ey;usVlU*fW z0su&O`qp&Zx==4g7ab4~P>}v0zB@Lrp`@}+Qbap)?IhszzV|YtFH6KQHMyggR0N;2 z!{`@^pnA9-4rDe{%{6%54vnI~pwMu!Li9XXXDuc(0zuVo5zg zv*`TAtP`4uv!Jq8Lh5A@qt|?F=Q|#1!fv;?(NeJfMN7?zP?Z^WW5+kl09cpMLTTTC zA81`QBNg-5y?DF)O^Mhs2*uofAWj)z_IyBQskj8XgWsrU-LsGJX5-f%J`zpM75Xez zg}YRq4rz-HfS(A>UC|b{DId1I&ZNSs13?o?s&ss-yr(B6Ldb`;$V|c_iP@7Z6KzkD z#3Xs>lYtw5()#*3^f>m2Jl|4hqV+rArv8w9x4082kd#~e%%MnGaeioT9k##>M>F}n zq+g%2CK|8(^e^b?PlI3Q?8GC-B-34W-&g$b%k&whlG#Y63-pvoPX@r>-pgxLHFBdG zsh2|2k1Q zmXKHX+YA>Gp#sY(uE%!L`}Wz59A#vHK*0x?&fGpMv;iDBz#jCU{lAjH1Xx@_wDkqd^*NeB3px5HSw zbV}YC1OLw1pAf)!Po_EIB3?Vk?R zczhfWa#YIXyLJo`s{@K)fYtCr>>VILGcIkq0$?9%l6tQz`wfkjhs%Ybv+H{*xun&_ z2B+_ihaaXmm3aK$4E9IB1tA3lLoas2!%!w^{mT^Br*MxV#Ml{X8qRx@16RLMifK; zYS6Saum76z`+Q-Vdy+an+ONZ^PTz3(7{pVY+=|BIOzvYeA^Q6I*UML}A;74UTAlO^ zjYQnKwDJ8al89wjTpz!DvB!?Uiq&!e{EqRwqO??8K>-z)#XxY|ev09N!%c8bzdhqdc!$ePdp;TM(*JuH)qg!W;2}+ z_2CFq!0vDefDPhU0#_vMupqgn=C20WqBl*;=0 z0sZ5Y!bwMnrd&Ep zwxx0JeUhf$4F5hK*X@k0v6)#{dfZg%UJe+^v(y>zm~#c}v8lRRW}1vMyz^&RS$ua) z6yF5WmctOuY*d$+%z9}Z6T#3Me$M35?%PH$`Ht0nyBHIT0Sfn)2N_2C6(cc`G;fZO zoEm(r?8croQbT7Jq_Lxi5NIw>WMC8iYp@xUWvK)uiPVoa^vgdlxTn4t57=G(=%vvp zxZb8%`!3`8;ltZds%;6W7-W43kz)D*%&6|oA5}T9Vo;5l1i9tLU5ux`1pa%Mye?wD z0k)Qdk@K$9^EM8N=ye|LV5{00+Hlp`6ZX`L2qb~5P8PUCKS8%HUNc_${tDw2u{on@ z?(k)STRJ;xNRf+c%8Xrc3!YS?TbdSSiw&szW*7h&8qp*>9+zLv%)k z`d!q9Xt%t8jNJEtPLjY-J%LY5UL5LhH*)eXh&0vdo*S6>CFvn()bJ>%3f%a#!_t+> z|1Ez@SH(h>_n?5voD);580VXnq44p*{Z2LOc=izlav3IvOH`Ksc}IW^%+djiFH|u& z!1QbSW|q&q=(~gFzNi@gZDM>mT;JiKCpHi1v=+&}y)p3uY|b@fy(qrsug}M@Qv|^G#T?BM|HH3 zS~!8eC2a}5T&tR$hKP7avmYFL6ZoBc+>^!4D>BXBP32g@hHRIzj=TK%z|MOo!GD-3 z4!bKG? zTC|FDfcwVhn|D1@km@TOif~Yk(?bZP^{J9>WR)XaZ|i#M;%UjW!rs9y=ZY*gY}TWS00Yry8q=5ob0aLf)q3Azt!w#6nT6&WP{4gLjH*(`J4{CwzS#r z$_=VHY1q(e_=(%*ed^J?V|VPQwjQ~0o7_*fU)h9#2{X?aD-e1MJjJ7S5jPe)B6Wme z;uEr`Vu_!9Uj#J_tk&KQUVqtg=n~NIAqnZqpU1PtQ$fvXwS1eTu^j4nY97@}wk_Z^ zlbB}2v6SN7Ud?HKGea7gTjqvwDveAPjeIm)izysNXaKZ&P};n4y$pPDu9iFqeg4Hi zec~m6!#1QKe|z7SYE!WBS?uY*xy`#SX}dwczqEMz1pZEZ9l-KPqNCe%GPiP%k>zeq z^D5_~Y{o_zd;dp{Kj72K6t8Ev0;ic-1YpHR0+Z+E-&a$yaJ?~}o1eZ#igoPh(26Sf zL#oql#%2)|OH3^PuIgcY4GT47^x*K1!qh`(R|K1S11>a#P>R`keRSCZX8N-7u0hxx z>8jTWDHg&7zMEF~RZ%35b3Gx-fHXU{u>)P7^=basp~mZRL{6}Is_AwNM`}DyzBUC> z_yN)+!=rLjT>K^CZFWU$L*TQa#^EPR#mZQ3%8pp}{H_0sk3C_kI&eFYy3k>X6))xM zd9&6OBN9MJcnR(y7rBhF?HItzm~~ngR3u&ua$8Xe5+ER`+$_7wt9b(XhY4pK5Vc3` zPaF4Kx{$kbi;F?)g%l^lF__C4KKJC1e&i==S0HrgJgwU4%A|k15yv9KT(yPhYFYdQ z%<}2-Q{!p_5XWt24f+kwBwm7wrYeNj0o;MX^J^@-Vr5nMBc%5q?3;?v>bQ8?oAx>F z9eFV;^rKFM_Ayy5*F(^sutcs4NjrC4P3qn7OB{63V^$*S+V0Ab>qx}TqvhDykkXLTEl&}Zg_hW+Au zf&G^y5q{+ewp8}lTRw}%QbqNO187;_H1)E_j4+xE-4VZ%=RRFL-3w<#H$fgxJ~Vsr zhON)5e#Gi7#))!2jG8%}<`H?3JiymVYgnok^3|Za5GaSxX>C`s_KP$vfo=p2=weNa zI{q=a5j^pK_CD~KpWq5TS{AM5yj6kay=wCWC57@wFxXG&*>)^$^RXZ^1JR4DHE{naCVJa!=mdjt{tF@Y$=#L*R!XUTI>D^S! zmjg5(fb|SqoR^ng1NHFR(U}WpX zNaC~)T+lWBtrh>leo~yAdZD9HHu1SWKWkg;OB#u6T( z5?|OC({=Gd<6zkfB;ZE;Ny4nCiwG}bU@FKm7Z z+tF;u2tM48u*LmtzgzJ^_lA>HzrXmiqx6fG2a4-4w`@4^m2Gz|qTHCe z&g;V6hBWIM^&=KIN+Jy_K0YqWgmL6py@%S3JmHSC^Tt6CE+>m_ba|_f37;f9UzZNI z6jd!<`}6bGxE7#R(o&1y&iGFzM3B=CI6et-E)T}11xOq0o*XD!mFb2i zGNef$M9fAnRwqYLbW2P{VWUh+pIIZla)-6ylRmBXz#Aj_;^VPUBFV8L zQtrB4=<8%W7~AtCqqD#kua4FctXrVAUkOLY=8$5VL-b2U+eT21|G|G%lm@1s@%OjI zN{h(0B(C>oX4>-t69OQJtEn@A!4HLEe6$fZn@><<_F%{$Ab}=0kCdIhs=68-67LV z`zE)=4K#p4>{G7=_A!G$X|9C*<JNV_Q3;#ormp=JAJaMDZxf;OCCfq7m~4K06@Lh0edH&Zda?(E$8;KolX5 zqLt$&+dBDlkHKpLFI@MTLcj6;3`2r2@ssljr3l2M9w3F$UU4-k3Ad{3xr#*YbFwPR z2=t+lDUm{khZPUs77Y^4GuN6N6W%G)tPw8!HSM&O?Qk+#3g&V|lNg(y?^mRt4(;~P zzPo#)tfLV=Q8?TyVaBD7r#6@_XBAT=%~)rB9>V{kPK($IcTxV0(r|R^sd{e z{pt=~hc5SmO0<7sPh`!@mmKOh>6JN`3jbQ;Be2Vg&X1FZTBOM~SExV8AV5a4gu3T# zZEZPy;{mcIx2$X^bhEd&_xuu1;&gQ2PnC#2A4ijJ!>AD`OlwSp&RS)h#cCpw>Iy(5mC}%2I8Z?F+xIuq369&h$?BJJ1k>; zj^F*76>#I23bKTaDF-T+v1NHsL^k`=J8FhDiLM8(*<w%ZjDSS)__ z3Z4ye)Zc8UE8Owx_@(nWa989pjDz9fI9iR)bZ7%_xi~okt-P^sARcX+KGed?A0zD^ zW;I{>%JkRVPRG1bkfU)fLqkI&FmByS(yjD;dK~MbYid~Ex}Bfbp*U!+z|CbRU31A- z6Ey5}1d;?jt*hIS2ZL<=@#-CYc+K?HoZ>TR4jqRlQ%1X1|9tH4m#x{6ese~DEe8EL zB@Np{cS*aIY%9U9a#3eJv3#p)8X|c{fzOSg<>) zBX~{DI*Bqtl~Wq`Xv1bDpw{H`B=>1atd_j-ARlxL^oWfC69F|Y_$_Ij)l~5p_pL@3z_Sv={&0G?q#gwuR1nHN)+i0HxI}f5gWs_ zq7}m)P=f{77^jJ`O?XQ#oQ<6z?=;1ez)Fa+dSBudiP9M0;3<0>_w+7$smj6!Iwy!O ztY3>f(tO2wvGqiA%`IvB+z053VH(s!+9``h4|^9%NFX~nHakHHeEJhF7OzBMxC32f zw_jJ!U#>2yobCoQV@qZoIB$&S%UxME;>Zb-pxZYca+|2AuF^#|=U1wI`hY%V`pQgr z6iChjz%q_#6L7?#9WfY3BOS0H$2h~LLlb*1GPI0;_+q3x%u<@*#-E?RVvqMoE4Sbf z`|HXGGCpXBRFkqbgy%jj*S~ov}kJYHV#KB}9ETq==C|mS{0t{i?)2`!K%{HeY zqonqosjUwt9MEJho72^QBe`9=Tx&aKI`W_}VB&Bi+VmCbQZ6W|B&()RVH+)P|GfO_ zB{6wiCwZt_tDHSN{QaBnhnv8JEwAGyvVF^ihLf+m`>g4J&KxXIvyDbEgVjG}xjGZv z??T|9zWsV?>I;_6$$$q|T&45}gN27TfVkyFjN3U_Z8&PJ-MG4@&g5=LV03;w{IwS# z`rn;lk769$7j&hDC%6|!A<7J}k(c-TNf#pjL`U-&w0HWz28%(PXKXX7wGvFF-(7Xq zuzhDBC+h58t|i8%&pV5h8$fOT>8jL<8v~tC&V?PbR@^@+`KI-E%b;@Je!?_)P~ee_ z7}?s2CcGne!TFRPnxpaa{1`6v%kq<%P^E2rz1Ce-qss0cRb+%?6K(ilvB`%w94fo) zTbCT&DIauyy38>Le9Ij~qO~6+`@4I@)lm-_d&QTZvC)lGI1pwwkd#7qhRdQK82-ra z?CfmyPXTxPt6y?z-PktBuQV7m?a}27o7>N!e-Uyf1 z(gu(|pZg8cbV{Od6p2{vY;Rk@pJj5)X2Zs>^K;)GahS+RQiVP!+JX3OIPdhU0iJMo z1Cp4s^73wh<j&R+~)Qt?hkErPub9opQr zqf?!7>Iz2$CFLB))`I}CFc*rFNEYrEj#+od&nv1eJ3sbt4dEDYd1*?6nG+$(xY5=p z3&Hr2bRoCh%~2Nux!{vpoVK9+t@OyCdG$sDRQLd(qOkaiitb1~&l*2D$rM$dCq1h3 z(+J=aiDxT~1GJ@Jbj-{oRU{4e7kg8Z(oDu{`wR>WQ7+s}FuEk(zx`!&11Ekluk6uB zX~`K+0f2u2$)<-e%Mdl{V*Bwn%4SP%BGs2s9lwf|+S6mz+v2zK*RglntmYpY?F?Wf zZYi%t(rbEOXUT9e1}fpb1E$uie_Kryu$4sY<40_4Bu&+L-~5C6cif@iGU9t>7x`j( zM7eRcTfrr@@bK0AG(IMuj}+RZK*w#(2=0C8!)1-xFE)}2C1hk&hLa3siPYq@wfjI! z-3?z2idb=9%yk{h!XH2}ef^B=qywPuw*KrkZ9;T|*K$#|%yUBwwvaKym~33E*JtwO z@Bcb+WQ;WFR#&AZFA=@UN0@-T9H6OH$a`>9qKC%qk|9~T<9ZF?G|zZP&eY%EUjI7t z>C;yo-Ot9uGwnEUfKED93@k9T>OgN~&V;wFbmFMnneHcLgFmKZiT9^M99586t0?qm zWfvA(7dECIhxmYe4@-yzC$>wPgPKw5h6h#TuELKh;~JLqQ%-4MYk>GB-e3`pW}O=e ztFPY5Ff~ol`VvNU*o_ZO@fky#urzJd!p>5fwxw7JtOB}%;W-W9aCo=p9WcW%$YUwQ zzZ&OD9#Qb}kFD33>KS(dwNlCrugy?!UioCn3nrP%UQUu)L)4Q?-7 zC+j9LtnTJv*&5fMQ=Rb&<#dtLoU;FIQ#xR29IKTg95SR6XQ}@5Gpk>a@Qy4MI)Eqp zJ_p_gUZJnM9P6N0Krbj>U*?2u_(N~%4v^pa4Cbexi&1RL=2UoBlc02it6fQ!ir#Oc zy?_5}>iH`Uf!LA|5pgDga~f1+fR6CC;^qnfE`*Accgt%&$MW@&@={Nf^w_E^elrNg zLGYl9#6FZR8IH{-1dN8je78zn@iQdz=QQ<=+(!)7_;B5ti1?`88)?x58&ih+LV?62 ziXoTV-*gGP9HY;0$;_MR5l&_Nb+E@9i80xF8Pa$ZGjHa4gQ8vXK6i==X-htUvm~fm zI=)Kpq2B-f0{{sIi3W6gC<#4}wYWa1{9KEZsjbv%jjQWOv7#z?vRkSPe=Ng7OFQP= zpi9w+i`MxLyh{}dVr&-wbfQxZnOJ9F7u-&s^ zaz9o+83HI7BvOf@^N2Ldpfm5R#&}`fbLTteP1%RX-V_xi#N!HB6RuBQ>#l2HVO={X{bU!xqZX<`7u$1}ZTEe;ZY)PL#oI zeG4n9ln0()>#0}7Fy}n8MS+NIl0hq(GUQpEA}tSekP}kbrsl;nUH9nOQhDPTv=f1o z=^spqKEX*-QT^@`Nr~sD{vYO`&)%t-dc`renQkLpaKW3ZT#yF_$|USu<&^fji7y*k zsk7;{j43CI6ef9}Ei4D|_A&COwY1~If`W43EQJ-vD}YAPl|=P9~UnfulJ(**ST;9X;uWfyC|P4JHfjG}}kG*l!XPG5iY3@aa;5 zdljG;CC_mxc02{}C2x!s=k>HMu3a!~JGKVFPfSegRvpZ^1c)$+hFi!1 z6CIr>VDFo)_QmGXp$qhmX~ymDbNyJMNE;1gopiUA`2#2K_^^sU2um?x$0pq!s-oqD zGX9DL*;uH8c1H>thIjLyahLmV(ks6(Wk_#sP>W1To)8B$Hfu^&CTqYG)mik|Ib?g zzv>GRoE%SHX3h{KE?R{0b=o=GTJQOy_jKwmxSh0ase;Tr)m&jBzYTRC|(NKxV>XHxcl>P?|hR47(H| z0qFfsue(hPbP+AN6SXi?l}*Uc)PjgWFco`Xr^*kIt^lie@+Tmm*(L;aNRykEcEMcg zaVasP26m|R$)oK}BouLM25Y-u7=k{CIqdvq3C~uNz)&!+umPxo>r@;R>#kf&6}_u$ z`TyWX_^j5tFltz(-jgGePT68oSE(fDS$s2=WX*fvzru_eA=M>2kWKKXUMupHSB}Np zt#r5!X83GuNe~S5x5TphZa7+&ma-u!-;SJ@mvD5zavOb@O`Yh&B_pRdSBR8beN!z=kjFT31v z_F#Qcn~J(!%UR4yjsr5k)TG4ifRyqiy1-3I$52D&B$8G~eIf@;p!LMU?ioJrL_Q72 zJh_|;)0FVTF)30w&|F`7DCg^)xBO)$Xoo9h=`)LV|_vhBw>X$S*B z*4O`%+ajO4gd>j!Bb8wIyNL=`BD9TIDUx=P#|@G7k*E8SD0dA_&Ch_MqceZtdd_3n z+jRLI#ZrD}tigMUJMl*%@=z+soIaoS*XB1(i=fidqoo@GVKk_e zbayjKxJI~qK^W67!UmtYrawDw2SZG1bV$34+ zE{WY+C&MY>QyMwdE%!^u@VCM@ouxRoaNW|k8T6n z%YHtOGd^Mx61@$)<3QQhia9E#(XLLHsE_00@7|1Hl@E@K`?%dLxbOxT`aLj?NFxp# zXNpat5HoSNQP7I_#+v%A0qvZ9&nGdQJ*gdxjrDg9Dm=c`*GT{UR`w0~N}YA8eQevq z*}%;@L3>CbDgn{yZx9(UB?J_FULY5n{1afOg7HhnIgc>xBFX-*p<_;$HfEwM>($nx zJb_@4Ur>(-J`_W8mfp`nU$Dhxn`BmnL?YR>3l#f0n{JemFA4XwIAv6qPu0z9+clx< zyCZ~WlI5+%VoSSqP`PSJ)qfiizMl=4X_LHPDPNRs5(~8ha$=wRdLK)jJUaOd*himR zX*5hs;_xaUFt7bmyWJ!pR?RfqsaYM@gwl#KWXUaW%P@^tAmp>ttj_if9t*r_p1-pE zE^^HNZ;+iQqHV2UB$Z%=6sT~fl<88%!0R%7cKRagG^ohYIRhCYvmUzSi)y-u+g^gE z{<^+FkC}oy`po!&Z3(P zqBP@8P_qILwd-4Y=_Jst#=jpO4@0LFUN@g9ackU?IB5#6%D%E8R5{}%zM5yV4TwIP z5aRu&l%wH3w=wr{Fo&({;Cr5c*Vs=JG-qqt!Rn$e#{`Mb9D$`^dak~PBy7CYRDevu zMUUfF@zDk^z%>I8>`z()CLp~2N0L_*#P`i;5tavWHRn0Olm7TQm5n6a@|R&0?K&&=Oi9laPXstEbr=&Z*>xA4PjGM@+ghvnl(GQbMf22B002_FBz^uAR0 znQDp5BheZuqz{x1ZTNbufAyW`Kn|-X9uC=x`6bs_;8KgtaT%3-eynl(!;a*vAxs7o z`o>><`rNyyUQG`b2^dwA$aw^XqJ?OG@~D>NBd zs6rm+(H%Nwl?Zj@?5*LkwEMAa2G*{#HCl;Qi0>Wm#7LS~uL`6-=OY9{Ga)AVTA5!c zYMyAtC&f4!egkcAU<1qkC#rtQCDp!0iL(rmWZ4WGlSPq9TNSWuDJ9v#PI(H4B zJJM*b8V^Kr(0hAcu=TB`n`OP);!?{>qghh4*FKqNT*;oGJMHF)F`RBJV{;P+^<1FFXz{@}N(u*gopRNd-$KuSWX|87d2!@}7`H+2~2Hmxy17u$(rHPaP+n%mqziFmZxPIDWJ`6j`Q0x-C*tGiEZdM0kWEMf? zEH-c=XJ2v^xL{Xoi@9w6+z5?tZLr;?qk;ePrCk{`#6H^y%Aul#Cn971QlJlk^kT00 zx4RJcOFwE%s)R2!M{RW7V#LMY!yab?Q0((qwW~ibTWVx%z|QixD#Q7?Kq!VgMusg1u_K{?;Kh^t*k|Nz1YWYYL1UU8Vt4mDc<@?%;+8SjIMIW@@Xc!~yW|Dy=wx~o zB_$(lYxx>QzQ#9d4cT-^BfcT$nZ73i&n!j3*SAtU{dr}m@kOc0 zL>ue3t9$7g3x?Bvwxhv`1RR@iGM^Xq!|3bQfZySd>})(gh40U8*HGJ^zo@8JQALs{ zVEZ_QuCgW5F-)7vnk3sR)`rsj7^vfN^LjxZ1VdPe>!FRT?=VH9y3J55zH?E&y{18e zrd#HBzs#{VEy!>-)|eQU>$PeD_u%#`7Lnl7QjM@yUvMbiGL)ad`)`1Kl1}$nP%IgM zOP=0@wg*qF!L5gmd)Loozs+O+!^j{{AMOq^7k~x~#r+tu1RAuoSon%geCDll)q>FB zatyZ?413a9Kp-U;s9Z3d(^iKSPQWToj}1?J-m9>Ceq6AdN!Mc&Nah%|!qz|NtbaPx zsul%_mkfyq7IGxru#SX}?4}t9xa?{El`VP3&$^z!YO)3XjTka@lD@ zR^2lLz^8G+7eeR(%_oz)6*+Yqx4*dkawSM^q#anyCFEE zZ{3{ql8f@-cAWfu-#-A?_OKd&`SUQAqXoBu)QRx~#ggBKd?$zqejkGN6@h+xz18_& zZG46WywRYv&#lq?%W~t1aCVn+l)r8}Y%C*vo59<)szJ|nb$k1oXh12}cDt6Xi-e82 zcI2+Xjc{RJd&n187S_Yqg7tjnb7e5$z|W}Othl5Tkdnfs(Nd_#ERY}vGlWdo{pig0 zryF&*r*mmL!;T~q_FJdqzB(2TG-vj+;|%!5qwQ$Z%rFx|OL>wM^3&8G0Lnk{c&px~ zoG|UTn>W_}dh&@$c|T$Ui=N)FMiu_a;(SdgJz?kyj9p;6=6!!#kdtIIj6y}cH9kI2 zo2Jyq1cn9#RXvZAJ@g0Lh!ib_dnAA%{}TAeQvZqB>Jv2GlQwKbYGa}FjII!Lbo$d( zd0u{v1KQ{Chyi<^#sA2)>;|^An04lV7UoWqXjGY7ko6AMnt2y@AF-9Cz_&lVjD1j$ zOaSF>HbdI)XX8|pa@)djZY3r~M5N(XiDW8hj(qT>Ax6l$q>@gK*>Fa{0=a~$DwgNl zj~(7QNSItx6lztt{>1p5in@L~nO?8;pO2pjod2l4yTZsZ-7*ptv?*=>fy3y9j8>oi zc|-v_z178YZ^G8ESJCgu_sdO7TdBHUi$$(M2?ubgw3Eh+QF2U8YUDXt;yDso#Fb0g z^+>43Kv&04c|1$}>>$^yni`4e!03x_Bpky$IS0QeLjS{W#!Bo-__nS-T@KqzkwQa6 z;F(_zO~i0Dn35oa?bR=ZP4u#Mc-7%>iRs1l#Jme_Z_h8r-Wks9zWcNPae-EuF&YYV znoYpTk+nr6a)ZE~eDzebn7pkfYP0jvJ0Kfp#K6D~(GtA!)5w;W)#)P3^OjNtzs%Q* z+D}xKT|t-iD2O_RV2VOXJ3`EeU5PW1i<7dp5ObjCaPOM8a{e#?LjNkYg}mv`@Y+LB zS2gII#jMKZQcIaB$(zeGUJ|Gi(cccF$+cD4D2pB1*JkpD-6#kfol?t@%tgVg#oV?- zxfQOMys3$b71m0)Ob9)NOq>scw%krd`CUkb!o}M$?{gGOy-vVxVPRO(VcNBI_!_vr z);zD``kML~MJde`v{O@LsV2mL@MH}00z%-7>1jHQk}L95CF_4pOfl}~J3y!_UBJj3iHfYlh<#92(SlZwPjaf;&*4{0#4Gy( zSyEvRUP*w*WSclZ+N!oevd)3$aMo@^clNR7lScay?ScCiUagL~2wTF>`k_cVj5HXk zH1TL{%YT-lAJkbi1A?lh;yfN2F3VYRx5)L<1gHER3$!)`4{0O6%O@v6I&H7?^A2d% z*{c{gX6<`fOa;khsUi7-=?NCV<=*`0s09jN%mYPUbRAw1oz7Pt;;T%7KVXr;`^)rr z`V-a$cOjsE*4@=*miE0%{O_aY$N1TxS4Usakf`V{@doa1BBFP`aNT(X8m0INXtD@`dW6yWKZlLh~{`Y(6**iMIHchrn zX{_xn5+wfJE?6xXIKiBUK`<7Lnw@PvG@l@{QEnbsIjss4 zoby}JaM_PydEgw1T1VoW{w*h2Pag6IoaZL+s6PKoTZnJ}V8Vd?h)nN`)uxz){i!4X}$I&kDm#UX(q`V56+o2GTwOkci zH$)9$txF|zcDe#4wVUVDKAkAY=y*yeHM(`qJ@l;52zu?d@drnv#*;;@bR}dw1{*h& z82kb7Da=wV70Rhw%mj*Ls}S59TZ9!fXk@WhkrC6h{j>XRI=ecl-cycJX4rzEajwkR z@A)BB#G)Dw_VniXv8yJ@z7rT7(LB)d=O408fXI=f;avLw4XZLwrFkc%=h=*qiBa?* zeO|}q#npPGHs2U9*3y>YO~U_U`r?ajLj&PcwGmG`t9|R;X6u3JXdZQwu8o@)S)=or zNR{2#*Yn%XS++$mci0jh?J%#h%<~M%dK|<$Gk>=!T}HuF)`~~pIP$m~IrURJYJF#l z8!wr>GcL#6U!yVY{gHJ3V5Ie=r13eCHyT^PDiy(hS z)o7cH$sti+&GXofS7s?Ua78}=X=rFWBviU38kq}9JdS;F*Fx({s|!-nXm)$0&ww1qs0T1)5;*ydAY9rj(Z`6K(Zo+UZN6gfw z*NI*MILwQBwKjwe^yjm*goJH*UA-Hv)p5DmoUpU^z#tPahkblJe@W%RuG*H*6tYJA zwJ_W}Q>JZJ7d0XbG*|cb_EbReyd#wX_QM&1nczOVVmilIw+cqaf#{Ta0Yg*#>N(}~ zo)LX>F_Rw*^Yi>w4pSWFg>*eFC+uTNOxJ9uJ)xhCSF3wvA*Z!HO({lx5fi&V7dEhc z={dfxUb?N>H?HEAz_iS>HKua~^TI;_(n$P`;!#<`&Gtc@ITZHhWN^+l$9WT$v=1i6 z4FQml1K^lF7$fClmDB#CJueRlh~nn-zMII*X>Pu+W*VKIR(k-7O0aB^U{;UgG-T<) z%fQT6!uQ-+1+qds;^dsP_d?$7DsV?2K3f1-6NdM?bo`HRMgaRii`Wx4rE-`9d%d}vE;np+O%&esm3)%^2%@3 z$&|Ji7fa2O6cJgvx4wDvX6W(kj04DpAgkyT+erCSvgNlD;eU@2(MC3}DiHBwOW$ZF zneq7RMest8!vQyr)4bW7c_d8aop`V)E|cPVQOel81dd_azq!DlAy%JDdqmPlh^0?I z&wPxo>`^=@4j9q&+fwD$15-GHzhqUuNG4uJyiA$dQ2GG{@#yTVIaxs#FcM}Gv2)tU z?|Q`Pw|VLY{R1{J->S=ed)3-8{h19ZTf+5qu)gdPU42VLvTS1OWbGyKQshQ&6y$t+ z2rj6lqGBu>y$_Q9tsTE07p-Ecm@GW2z}Pk8(1sL@K|)rRD3{U_dIxVBdj-LkYkr-b z1y%I@Fr3-?GXa&L%DnGu>(4=h<-C3NPk-iTtfc6Ix?5>6c!(+r8{l_j~E!2i{^>H2lF#W7v}dS`5Iyq*{y=<)RM zgK7~<;h@bB2DovXvAp0o4D+tbKXx=8=~=!B?#4G_2-4}2>e@BxR(>MRVDNQG)q(JB zR#Bi`d z$M0@Ua?MDr$jGE&Fjw1~2^9nIUA7X4d$LV(c_bLn0jUPK&X(3k2)XQcU9ZF8_1#;4 zt>G9btAH`23P6I`_rs9E>ezf4-zwVf7{NA+JllTh)1j~ra05)}h-nXaHPSVIJ>p4e zm0-o}P+AHUK2Xz^=J(ev$Am>o0c}^U6+ozf(ZjKo72!Isxl(+vps`r2 z^R@101Cfqh_~`7`bcWbsQgPRnU6mVdmGSJPguB@Lk_Xuno`;&{vxp@RHPsa;)zZgr z&{OpDU4awn*7(_RyY}xM+*ksu6PWJH6-7ntbH%l<80^9cK4z$ZZb3+K_O0XNO0kc9 z_qft<+Bqul4$c7W(P)@f)^F_DQ+$JK<&I>kj#QynRxN^CO3r5OJZp6sypHg9WbGn% zW)8%J+$KR5+m?WOG=2a(64#`>6Sc>V$AT3xF~qVquZ85&nafCU9xlb9rKJ@eLQ`ch zh7c;2kX=wB!P0}Aw8MJcGBZ|SNQB&FJcNjVvQnUku;$Vf=qV7q&5eNrX)BVlCgKlv{P zGQ-$!dd=Uk^c(={XwI)jJxBk-Hv8f=r#r6K&R@!z?AO#A;hrlD%?!7i1K<$-I{qy_ z!LugM##eS0DB%CfPTdm46HzWGqJr(8^IIW^8+A zgq{yJC{K{S5J{IRDJqSlHC%fRSp=&hnb8u#e;h7z#1>-71#fJ%&J>wTX2TM)A+sw_ zxbDVYx_u5CYNXd*;w)lKq?#kS zm%Q55`%Wwb0d{S2+aI^uR;*7yy;jEHx{HF2UXz;WzP6ce;E69f*>Gm1wEb_Kat?bP&qxKM8=+;`Po^vdFgJUlq!sg zWXszylao8ILSxzzT}T8zBlNpQLZWN{{O}i2#sEiB#idSI+=|69LW{=`dB{TjB=rR| z>qBv)dd5rK?fDznsn?O*iOC&3&#Y?*vXQ;ja2$jX&qng7S!TD!3!GATJ_h^582 z4?EVnq--tD&yX|RiLp{9x42qh<4I_y!^@B&vHW>T=lXtcyP(SV1_{J z{8H-2&%ZqqLxrYj^LeY1*l2mMlA|=8p*WA}a^S@MxXSg)S`6KzG(CweX4V{k;) zdvI{ca^nE!&B)H+@J-Fk884i5S~Y0>GXW;XrO1qG_$x}6z zrlA}*`Tv&Rd)j$k64p(exbFo+ki`~9LcISq6MGDSD9|KfN*L5jJtOpT>NC(%1y~eA z?8u3=^vg)C312m%;>XR!#0bCJAho7e{4C%go|*Nb>N2<6JAK_fmt8FJ-Gz)!VPKq? zz8#jz%d2h^GuU5HnuaIe>d~bZdV<-w;_6iD&FhAoW(zXk8BYXF$7Hhk2JC6 z5qB)zj&lygzw75WY#GPc+-zt;Su<_~q|7g75V2^SEN#a|x&L`V@KvDEZ2tx0>;NQ6 zlBDAc`0(HdA6Q+@A}rb6l`eQ+`l4YNOgE=l8_kDX`(w3_oV2W;Q6T(?keW6kMFa05G=|DRKgO*7g%HlBeIdigZGWAvvcQ`17SCfS1Bv!SzU*44XRvSIZwit zxk>RGg#RE-1*ymL?*QMWiOZUX6$|s~&W$tyW0`1B6X0IY)(HRo!J{NEocdT9^dtVO z;U*@hfPj?+*&JUaYMIoZ|0zW<%0$9Gpk6n=hvPKL7(;PWd}DyDlH@7#L-P_rlAfqM z5*?7^CZ@O#_e5aX4!}78428vr`cMLT>5t5F~Mpv4#C?EgU< zUJhj<%ub5bKR!ZndhjK|`|OX$q0=vaKS4*($z+^p0mRtM!Ppn>WPD}xUsN80LX9X{ z!+=I3Nrt3P)TodZ=mo^h(Wuw&#ZT`rAx(*TR-c78#-Qf`SjLD9=kPEd5Sc>*#cw<^ zFB$%SaY;T$IexL>|DcqmK)$i4-_roX&_6}@sjq6Zci9h5Jmv`^)5D>{1jO2G^$)6V zykI8P)M-{xCGUZ6pGc8KjI2rDW;$ z_5IIr;Fj>mRu>cD{}Osi{JBc7Jd_-nx1r)`b0J23MXz|@Xpcx2D8Sf{_d#?xV#7IO zz|G0r4FJ!d4w22~6chnws_{EE=z;ax6uvm9N@HD0*LtT}{6l{=bM7cY@}(3a6-HfJbge{~g?#IA zm+#MV8AaM{+G(?X&n7bEP=GHbk=B9(?|a!a&RKtXVKcV*LyZvt+~d?(=gnW9n7X+d z7=4&aYB#f7^FV1bE0n9+mMemII+XwcX^LseLZ<{+WFM?!P0An*0lzpeo_=O`dXrkb z&UhrsmNQKZtlqfKa-?B~+W}{{%S5adfu~aEKzLcsAl*N31iYJ4YlbzMCK^hC6-8L( zG`Eygd}%IAZObyx#sD7->GLEEC$}NP z6k(}8)R?43wvA9200k`wffv`oz_FH*IpxEQcG`lkQ|MfyH}>N~d(63VUy8xDZ1(US|GzA74YMElUue5=9SQK@2*IqT z&Yzh27ikPd=|_g`vGyC&aFp1Po@QdCSrXuC(2q-1r(s<|$xi-B==33go$u3OLc&LV zZ_Q?bk_fV9x%ZX{)>o{P?h0an-bnuEc3o>>7tfoDzlDKCTIp6e3-vi zi?wR)4rmkm&glD)7RaS@Q1&qx0E*Aka@ST=pzEEl8Vl4UIWm3-a}d#qQAQegg1Xnp zqI=^8x=ge_{D5&@C}0oW{cgZi`aVH{n3I;=)E$z$qWh+PiMs?rlIfUE_!hbCEWQGy zSkok-0fahjyyVpkLI7kTjXxfLbLHu9WxaXjkrjvo8C_ho%HQL2JITTt4<`2py~r0% zR{b&m7gc4Dhs+*!NpDP>bSfEEQSD# z3=t4H+hDsQ)UqRZ-1!<)0EpTfv>KMC|EW_Cq{~7y$G7_13AJ0+f`}EAl>Dx)uKZF3 zR21Ww9E-Sj3kevKwGwxoIUwK0``!N##!n9VG|vH6yvI+7~AHc<1M@M9DG`6tv#c+aEiUUxJa@l2lBFVBnjvNxaovFOdkTFYCBIq z2AygH@t;+!zytMkQW+>4Zvdptmkm<@AhYR2ctCyqc8t%Bq88 znF&~RLPk8dKi&M55Xy-aV(KUHQ%3ELWp8nN60U4N*Ti9ziiWif?^NcVlFlkU|HSIG z^MnbQ$=@r#6MZw(<0Wz`g$GyM2EwOtgQ>iLO_=t4y769Mk+7Ad#`#aVqu}~vCQjO& zi#OVeTb3EoTi@6{0g;T1<{OrlrDoTSe`C4up_H;sO6<_2nUhnX#ian6%(@Xx zIeBhnmx{p95-!kidLJ4nCQ8NO@I7G6X!+g7H{(|#*+kI8%fHVJRg%j8eWLhQOpCrOS{x ztFFKElxxy>wUjND++|zj(Pz{*JrK1WeJf!8J^yWGjwipj=YbCdCPSC^sK>k=$mWnU z6#cEDfxLB2Oz|n1XSw73@HKk&Cs5DaupXix1vEr8KCU+mHqXewATA<{hp&>5T{RoN zM|m9(j7`(G!`N-tFw7}Q49S;y(7WlTeN>!B`qS+JSK;d1~#s}y`sloK} zb{#wPG1~lfQ|(i3b>?h?m&qLY-Dw2k_(zywJYy9{uK4;_Jb4y8?_tt!qYSQ-!H64_ z_|YG7vO^jO*7Gm?+^4CY-Lgns8x<8beVc;ZnM*3*)%`}y^p`}wGNwOlGfOmd8d~Wc z5<9=2e^5{26d~ASs%Y`kf4|o5gE_@X=!X->*2<#1}dl z`WLC=38FctA=G5R1iKG?*9|jKSfY`~dGp%)HO{FpNpl%{?Cl0NT553~`qM8JydTE1 zRnUUFZu+DvHWm$Dv2rivNa8jgXecQM(V#M|T~RA&ZAv(9A-FAJoTq4X`DU+SU7%mD z>R4jl1@oT{mWP9Mnz0uWr^8kENL(8f{aP^ELmV@)c67?b!w6!nHAXYPk10NAM1C2o z*#ps8QfIKHB>)M2agD*Ml_P^UiL4L&7|yNcqNf=&1{SFf6I;B2k^d4&?8XAn zy-)v6hhf~NONP#=8k)l*y>{kLmDqmrb2BIMma*YY;h}MBQnN06a)pnmPrxT2hKupE>Gl-H{x1K>lNfiZrS37H=y0 zHr7o-8QSbtd34sbWW=dvTYeL9uPB6K@33JU^BD&?PHa!nGcsVWmHzy+D?&X+Wxt;l z$~(XM+0l!a@@!tIUH3?R?NV8h#PSf{v^_uYJ4&|Ke+(f(CW+TtU!`I|rlbf;kEstaDlwMhnDd%sN zs=&$HORN?xRp>sN0iFB}-!zl??~W1L^0FurMs7b=up0fUvvX#H%a{@jd$5TkkG)^( z-v%G&*5ne9-(RkD+5BkuNDk7R;lH?rcPt(WL9RHaH7|ryh5iJp*vfzf;J~vCqRjz#^a*T9&@w z|73$vuE7>s@jQ@wGj;v^rp{^(cr4rmpK$wJqFXIC;18%BcJy88mD&`uGfV(dMr&}e zmGUU{=Iq4>6`pmm}!rC87Ea5#0u|5m-v28m<>a2tC4t|&*-1_Y@e_T zd2pGZLxa8Io)9w{>`imfXT0^Jb;R6gU6^zsw*jop} zz#lbB1g6tm2y*PJ_;g>C8_u&h&h~TS#>(7M#*Apj{xnHFM@lW<`zlg*qCQVn17&)x+=w3mHC&X#(HV+HS?Y-WW0To4*yyP+QM)Gq=4e z+bqK`RA5^zMw@T#MUQLsI}5xdR!8UpPa73dXd|Ih(gP>?_shOlHo-cfJ@#jVgrz#; z(0JL)jk-{L2qmF>a#_l|T>5@y$nPPcAG!{AJT`F7{aZ(m`633dKSf&5PxH1jN*c;# zOV}H_31F8DdFKX(_@o6!thN9z#4RC$VyW=cQU2p@2&Qn@y)Aw4h;F2M6k_G!@LL5{ z#ZV1c(9zR>ae#MFbdoHlVdIH_Jn`l?f$ukjigtHIOrZ;*P+L01qwDp}TQz3eLL96= zopsDW_uEoJiu(lnE&@Mu6s zq<$;=U&ojI_du&&kJL}f8wjO8_C3b*Vm@;cbxIs-`dy}~uNBeh_pXk_!Vebr zYq&2T!q(RbdMbCJUI;qrnC4^Fn1p6oD=?1`rR@{3QT}6V);Mni4o&3*%2VYtx-sJ3Eg}#9T&)*Ep6_D|Mn92!{;r8#bw#%7#4m6ba45%Z#1Hcj;UfmM*1(|booF*1R8PoO&r#G*u{*i)2y~GHI!&S}Xr*r=N z4cBnyWm+>NV2ZI9O;mP?@LwSH??qKny;Ia#_ig{nV6;czE5pHw?9ugR$J~PkMSl?B z1Dz#X*m<97B9Qtp1OQVK0Kq3GHz-MP?H9vAW}9;9MVW4lm20ky0~QZYCm}bj)z6iP z#1I~bi+~1Irpof~GboQD5Pa;6$$!IK${^y5HAFP$5m-7`}d{ zx6b&d2TR^PI)or}mpLa98WJ%Z_1Od&JD$rZg3ta&Ls-w_5Ph*7LYxNfolT3VrTyN@ zcc@qPl1HA5P?JTfE&-Bwtm(Uuw4Wdw*cIeK8vo;$pjXw5E!e5%mL{ZO4-e4cDb=h4 znKj>GUEhuJV}Dk+CAfR@w$d>QhDC~TpwW)aU0V3X`3IU_0eW;H!CC`BwlU^n9ph~q zRs$)PsEMwJEjvb<%@=)x_jzJs8tsLI3&K|vxOlg{d~(0cDH(JAu^9nb>p7cp<@3?9 z;o@9Os_ltX%2gnSu+vZ>9)k5#o^qN4HW5@lQ&9#JE;0cpM2}%z7_phQqM~YKN{&T9 z7glXr>(BCOaH(diiczSLljtzSm@P~!51ledQ8XrxGQfoLmsjCBVo!DEy~rUb5^uoc z-tJ=5@s}vx7ibt^{L{ggObH!_ zGKYG!OR#mriM8c7hpgGaVG|EXX3ifdG?ncm;Kx&E+(@jl>PjlXgMD;m*?G-*q#{q# zB+i;N$8zI(Jo=8$<9;YDtjDB>Rr3=tzHFjXU98M;2~g_P0Ho`*-Hek--~^* z!47ZOwJy7vE+EETjsowiE4SUv;t#a9lo|~VjV<60UBq7sJl`zkt9i2H5(kZ_A`~d& z*a8oTd;~6k$B}J>O9o|qV7|PNVM$Gcw3Gfq+gSVgz=^$kF%DSKnfV@RnDhOdnX-@FNW4PqzTx{L(M1Fr72|@= z_H}kqH9iwDJ_SIbKGrXXgr^JU|JSG;P}5Wj3$b+cN>3(9YL{mN)0b<#4O>#J!PW*s zo@N)Wlrq%Fddkh5Ot0I@iLK5@48`$QZwRaj4vazUl#4C|uT^^&=T1&7haxb#O! zk~;atPC3UDRcYp{AYvOfANk~lG*7zg{{Sf9`k83;QvS}#n;eC>z6eNU0bCbk*Xa#j z9@j@=W@dCp&BsCwn>hh=<3Q;0+V%aAXFTA-4N5NjF_gl!eG6z$dYhd0v*>~L(#7HY zEm2sQr)is}zv35zee~3!#)XLq%N9sLEc1ztghwzk(-hYv09Rn&ivRCfRj4`1s#CT} zOqfME_ohpS1x|sLy+2VoP0QZK@o8$(;P+daWfa`-(K78e~-?FJxIq;=s z)E$-AEn$*7_1b+#(6#HR(y-4je?XW)RfaK#CoW~a*xVPZcP`88AFiIoErpc8l!mfb zFz+uqKe}()+QeY1SvFeAMb@ydcea4bq!ShxrTF=VUs2}o(X?Wc=7BEXBqAdD#}mF%#0OyfYpoOAHtF+ZZiZ*ZTxXnvMClcOdTJ!B4qP zs@-H2nx;-eixo|9b2~ddHy+8v9*tq2mgAy}7p~~S@k!}5$6x9i0Wj@5oX%cxadEp= z14XaUdL<)oMYKToo-iiv(T)8|AZlAX+e@mj~?>TQwGM%@WWH4Fo zC1aRh*L6Qr1!$lsC#UNn@}&`6FE4nB%W)kzlj`vH{BvqT-rvLB?{D=ybv&u47_dAy zGb5(ZGv6I)3n9j->0Sw%)3oi%+J3dPWExK6{d8on_v(b-z8RC0kMoL13-FaIH5o2p zpHDduny^9yR6E%Ky2OC>0J&g3TvRC=vtS}{sw8osOysMC;$(PmNllIA#nf%`>rPZ% zEodX|tyv9-S52|mIN8e(H^xX2@lzQh24`b}odT$0C!Vh-cTk;<@?Y04jYu-H=mu(u zB{yEHnOU7#NlUqd)01G-5c;!Ni!g%WZ@7}*)?+LF2!|rEIG_3v$ez5>=;%*wxnkv#3S%_lqGE{@roX@!K+{-cx9) z5a_5W;}BuFlDKI;P(Bv5Kjt~;U#XiX36UB+OLq*};%$-o0Pkrw(F^~YyXV)!%;F-n zI~X&H##K>9K0_cBD3hhO?`mx!y^7KodX^$^2vagYvCA(oJD-lomE=r}JRXYQ@A;Fm z*V&_%{OGk_goRTLL@ofP5{KpcUn7VJzG)eWMPdRY-v>iobV=12S;PrLpgl>vg_}f> z-#rd23tcAu$=S6enj1WzUHr~a!f;DkK^a~K2hl4J9zLUf4k?<9-LyaqZf{ox@Z@s% z@S5YgxL|CxTJkMp6nfyjBDLCaP?LwkUU_G{XtVR~HoAbECWx@;wqnx7gC&pw zfL(Sox~>tV>IrTRKN~KmwTB$(FM!+vaf31#MUz^Ac5Dg#?Kf&{_$h==naf8q$(X1s z`DbCA|7#JAYO5+B>(omR4Fe-5znn;@ouG_h;a8%!%5T=6j_BR?(1B7<5YV$|W5nP1 zmx^H$&a|~BwS^gtslHoF@@Qikh9AB)8?pBHXGrc_R!ya&sm0PP#80}hhFCpWEx9=H zPjB2vdvb>c%*>>jS}xrN&PF~s9r$dtXoMw9Y3&vLPvscjwv7ylNXEza;isJtc)XZc zm(#Q^?E%Vy@_YDO$33&l>8{JXSNs#}i|sz`w}Q%{;=EcBw2!iK^OW|f%jbGo?R0;R zj;>$Jd&dferq!r0*r(`J4^Pw}2SU*ExTF#BNT(|y> z|D!)W%g3HR9J)29#wufAu=FDXoyAIxSw$aUFEZkdB;ljoQSj0m0i~9>XeSB z)`E6(JZ+^p@Y5K^b((9ebA}G057Hp}SFF8?m$$QlaoB(1uyo=Xx5PHZ4*f#)%O!O} zz{X!BvS>`G1iJ|=O=)W6ZC{syV;M_WA!N4u50m4bo!R@1aJWQ;Ze3`N(+Pnc|3fV8 z>aPn_{N;k4UJc?;@{JijsmEXodlN@74HMvZ_am6He@BiH#4c(2|C__TdnOg(yf!j$ar6w@r5?1Rlk49O<(mB%C3C@*g|5a)s` zAR~*^gg&IMIKDV%_L%_+G9T%wKdp-ok1&egbvq49i28N@Wau@)N1Gsdox%Zi=0N91 z;8YMMhou}f1L+5!u|EfG$ou&C$c+qbKj!q%ACJicd(}-wsN~!nd*k@gPJDxA^mgwM zjYzb9q_gu(Q?y(8Z(6WRn1|}`C}iF~l`NtLCoE@41evfsu4JLGD!uhk zy(;XPndON5n5pe571WM_G6DG0jW)X>T&vz`S1I!-$p>JCPPYr6u16u#v*@K2MfjRuuEke-ICzgjeC%JlnI;{_830!-#yI6(5BZ9 z2^Aj6zpp_J}o-c(5QQ2j)=*~@^3bcadxcO`#*qoQU~JQ zIVPn3ih&q@J9%xDu;oJ#KOXYyjUf6=e`clEHp35Q`IFgN-Cobx(gXOp>JFUqpslUI z^dY&n8JE%OSKmfJHr9(lxm>RG$KpW9kixU6J!6`pXRPT%idtjP?N|hoF>{eo2wb z-6)#a=|6=mT&jA*I{XtceLkGU4t?@{U{*zt3EDrLfgjT>;-=3%Jw^v*rKK%0b&idD z`SaHo7v{v}-&g-qde2SgL1-;9U^xR1L**CT$#o!8qfnJQv(43*E!UI-je#rDq`=hu zb37Vy$v~;?J--M~ z8{^bXP*3gB&D9QIp*TwoQeA)J)NOP#M|QfM)}grYi;M)Iv)vzWxZQHtWXiA8W$HL6 z^*UCk@{Vi=+b{P|Qjzpv!LMr&3L=8K**pVf7k)JQ!FxlJ0xK0~cujd+ldV_Z*KZv! zDhq<70JCkcq1Q?|Zq}mnDabogDeQ`JNPna_OLPTNl=uFz(-<1>yqfD!^VVp3r$y{c zW4M{f?{QVC_2S-C4$y5Zk8^TWt*V*}*-6n14-h8^WG5I(`V7+ScC%;Hwf!3xpeU4t z4=G71NOW$OtYAoehq5u2D`6(o2iTtb1^mmHb>)sPt4J8az){HyS6cX$>~(*a2OBk@ zR(dgwazCkF)d4FklX?@OKy6HQszdc zqE0mAF+QJe5PYQ_q{8~iQUf1ycP{QBLRYAnqid)W@x4GC`FN@M`XCICX?+f01(*=@ z{;NFR-X2OV;5>{{^?)Dz))CVH{zG|}TW&YSI?aX`P_!H(`9!SSVV(b(ddkfc-vW7@sQQ!&qvX02C+Q0v8Xj2ZLP5Q~W6{riVw>PzD2q(p+%8nI$`>K%}Q_JRs(w z`Yi?t8v^%~9HXa@t5Tf<7Ep4#4P{Za%M2Ew>`CFJnZOg@{=JzJo$}p0RJ_XT`*Tk? z!JMk8lscMn8_D}Gr~?Fos%mQZBa1v&B`q;{-@bTWPN^4gl4tx8VF*bxSKD}5{9DYI z^eHureic|WQ75muW0B!+@bB&stO%(vwRBuX3`m%aCTI)wjaV9n5~v1GSr7RxVkC_h za+j|cY9)*KPi`nr@YnywpA6-Y%NhqyX5tZJHu%l%vwM=HJ=s&Q;t~@b#S32>RU)yA zL@He~roX-*E+0v9SuE{~gV2@= zfaD3C;ZW}6uD{NJOU52+#;dHX3>RLqFLk)Lha&a);Jq2yi1jBg;i zrkKip5&E`Tk{>iVYA@koPn}gi3=BG;Q@CY){an>$1>l3Z8ald-&^OBYV*SqRQ_y}7 zk~rZ49de%ZxLicDxnCbA7}t{q({@-BVIc3hmo6Ev8L`RJQ1bS!94Vk%`k#}Diig(G z%9PSnQE|*9Y6shF(=yZN>6Qs4H+~@o`Hv(}WBP75Xw!ex^L=YQER-Y;eOX;m5o^Q> zy_fX=N;(gJD*ykDSDz#$GO{JdL9%CLl^hwzQDmG$$T{{r_EttV9kOLd!jU~PvX5P6 ziex8yk8|*Q`~J=!a31HJ`#$gcHLmM<;WP%H50qmMH)r_jlm&}$dWF8&Iqe1NEC*e1 zrB|rCEF)N6WJ#D7_XHIhM6wNn)+KnjK+6Pc$5Eo&VE4YAabn9Nv;BSnn+bH7rZdmi z^kLupePBXCr?5MT-FJr6?pR4$*it9;tM>)#QvjBf3fmFaN9l9Pj3N_b9`PMo>;_|R zT(3k1{<7rjpASn-g*Ge13JVLvyWJavP;ULfxCtdLkQeWVYfVOGv`C|c$PP){ibvgA z&ZpZ9%d&|MhIL{h8>8dnmhFj`3fIp+cM)|LDyB{#cV&HDs-}{jFG63;iHBCrrWN?( z-|J|t*)f-nKxsM&rzb>{9KDM=8{Q&QdTY;)zEvAK)Gc7l%@za zl%|jVmw**nZ@%A+76zesjr8kBhd{b4Gsub})<1JRT`1M@XbZO2@gb;4nq@;AsNl z%$D0}cqOlj^PK-~8h#_n6Gb(*gf!f11RRMru7 zSkDLIWlZq^srw(8>;zzDT0^McOJiHD>vRhD;r`hj?%XD#CR~rSD;_IszNUsV(WMid zC0OHL%IBB%`r^+8PO@+qiH9|adEd++TC>fC!teW+h4{}tk1W!RA z2%*q7HdXuCGwg)7&08jnV2+y4{Gj!yaE#v-i?F*M)8D>*L+6Yn!8EM@Eke}c#HlJ< z#@l|o-_ubS7V39OYIDaV&(&|~cIbZzQL#p-U~PI6kZqJ+++pYN9G`pT1|_B3KPTz@ zq=?i`c!((9>!B9lkx?A`@q?)!o-s3Bhl*~{J_MYgC&ROXjOA;qcI!G-Y+(0dF`$fI zR_=U)M+>bC0;bT$B1RB(#Gk@>-{PYbK*a(c@UDT64>4-;BStEMlmiSO|57y5q}JlT zF0r5ITJx*UCw=_%No>dF1qCX4S2%$9Yg&r=Q{4zh{8rK zrT1hyChq2{C2q8qDN~N4VaSWw=+RC z+@{rNusZ=ctwHSU>;ctaHh3R1n$WR$tuI34%B9yIeOqSIKm@fhMb4ONAJ#6`AS7$` z;oHcEWsNue20*y8{#d<=m~OVt|JEk#Ln|HKuIMOuHK{_8C@!}*q9Zf-t=3aw{zHia$7DXl~?c&zoO-)UX0?6`tEf};h7X&G?m4fw-?2B4cf-C+pG$e!irSbMi(Y#iGs!<&f3oNu)2m3Dq23xT^_N_# zhgApUCSvd=)pp3*nwkUFZH06ntX^@}1ZC|6tWMu=70uy8`jBLdYDKJ|IUuZqUu}eT zY>|W0wb&*xEV%~Y_ zG$EA!Cr3M5+cqH3bI^%?8~G%bQ%1C2DD7Rcj6e-g(@m5HoUAw|9LT+~hO4JXPpp1^ ze>3v4VR5G;=m-8a1C5#IP>G5^XXlNH>l^B_fw3;Mb7x12XS_|M1!0phYT*;FV0|{d z3WHNsQo3D*+xhW3%=4Ig=Db@e{pyut-(3zr2>VfhE7-3_U$Pwqe>Q(S zTBIKe)VIx?nbRT2a)$2Wb|yjPtX7~7`+Jit;9>A5?c$m;`8MH}qHEU-g!H&F+mzXi zY3zPhS}P^p6I9M8qFlsL!x>g7Z$Rz28YhEyF8*+H$n)hkl!$h*TQtZ#Wx4LXcY3;U zcKiHtrR`9%un`#r44jY?CVqbM=OWGhnNW_?Ga!?!GEFRoi50cpg_=L7CvK~4yPL*+$2_|oGH8c@EsQL;mj zJk@+dPqwtW7#;PK8IIGJ2yWl86&qVy;WQ6irpK`6eX<3=CMe5R`PKEP2U8faU#$fG zKmsV)zSP+NaxXf}V(n03D|lYHYUREpR&q5P<_Rd=lP-huo!*tfv9g)1-Kh+xcm7|k zOdZprN3CPKJ?q-LHQyb7xN4~wDQ-D1Z^ZYt*8K6~HuE_mfSA#NkawS^qkhNJ5GzVs zd5fyuKQ?fzFp=x~#L&>TKCLaKCE^70VRBJSRCI<46TdYWaJrK})=@6amz?gd%nJcV z=X2p(q?*1pPE%id`AVR6Ns%L&SOs+BwLX5~>^yGv@iI-${;yG#0|D~oG{J%GI zqz%%*#-FV&j=@4>+-8-eq-5uzTIk#fnD?8NpN4M)oDofuq|F)em%xrMyjCL!Vp`x> zJM)GdtXX=+EC;gc>=X3nFO`ZR(xoqk8_sGvddE*#--|deaw!@^%}FRq$eT+4Nr*@kA*F&eqyZl)r{WGDLIF8f-?qq7a8? zy?iRgcnG3#Vp%IlF|zBm#Xoe;doOB()PoRV-;30Wq|e=a-I93K*qrv8!q;oQUz;jJ zKY7@`->e88!otcB+@wW%@)dm@Ce5Qa?JQq_hNOIQWe-nqE!>V2cL|?OUO`kG7mB^| zA+FW1Vs+2yl-!Dc3=ne*HQ+vuzAL6CHoo;f_3KbnpIVqqXQ@fEf{@TirXM)EPi4PH zA+>6mYuIb+p&c)JBz_LlI{IdnmX=Dmp!kZ!HSt?2X2=-g{#sk~{ohtSlFlhV)xqhz zE|&H{R306)uFWEJ9=xZyR9IcF*XTjF^xvNfu4fkg+Z=fQf-bkQhw_u6mY zo&F$ud;2rpPGXWWFFy79=q&0g7n!i@Dd`9g)VTG~EbpLNefH&$vj%O`AcK9K{;DS2 zrhL16q-BOzXY!xb(p2O7Ku9mfnEeVyN25GPgZrF(H7pDn=Lfg6RHJZkt*nGle2KnY z7fu$J@lfirB^-C9_c@RKXO`gml9DeLkLGHHfLZ>4*u?=7!lu_YHZn)Y9O^sK^&FOLJnRhX_5&kK?H4R8qvs`l&>_iwTnK!?!ojrTE zb#e)>)Ndw*PZ7DM7S0tDw#iPrRc}~K=;^~tPi_@!rK>ZCl6AZj?E<5!fvd%}_PptX z;O_bGE$gnz9A`V7ct)s44^%}^JG5VPaXFV}XO82H={(i1FFm4eU!m#`QRGO>kSQv* z`*^-5fnV#Cw4`{p3YL3_o_~{q`OMCOAvoy#KQjaTg&6xEpJ&#Abd0t-l(puO^XNk? zMht;BXJhbHunhJ;UM}cQh`ekYZL5n0VW#vCDrx7!ZS!v=p*2_>Z6Jvv{d%XnT4y70 zi9VtZd(kjVH0@%WloMc`Su27A&yii)=}+_>-9r}Vk`SCSH0-C2j*g3xS&YZt{{FFe zqjyulQ_^;G(CZ=z#Yj(KVG;&N8(1k;YCewf$G(}rPO84#DnMJa-OPp1l>-+hg9D#?`G-ngqF`pUHic$duxBM1?(&)K`OZ4 zERqx$HmlRPO6wFo8?kJ}_!3&|*xOQYtmYdfwq`%bwbRZx4eABHlH0jlZsz6Yic((N^cN)4 z@-y>uVO=XdW@M*Ns7_ZHPj60>I%}}UBg@vTyum70BM<5@thRi~W_(nUP*TUaUf);0 z^3{8LeB|9Y->3hDCJ#n~EQ@bG^ZT(LYd>`}>YvtL_J9W5?1UJiQ2BlxW-b4{Qj?+F zY&z#jo*Ya&KKT2&uZ>n6bfCU>*sez$x4CIfKiT zOvh!!*2-Nr#St!GT%(bf8=%d${x{{P^*6gWycKGs zIMkylI@<$$g6B0=Rh*3!uiP|xV`m8*SRTdXy$sU#WcDgR3O&aT)^0Qr;ZpptI0Yx; z<@pLpHGNUPld)>VWonhGh+7~g^A7#>c3!J%E^+k!T=36c2_jcuw58GzBE_W8 zsq%w}*)S26C-56{7$YJ7hJa;BfihKS+|`iB7Gsh69!(K1zL>OCD4vm_I<8V&3DmLK zmp`ih(YdHo3^)hgj|SYp>ID(;XvqKHNa5i5v#}YY#Iya7UacwRrh7QA-np7U78B}s zL^|5(h23GN4}oLnsR=C^i3@fHv2#*N{?D)7WW3frq#OC!e~v}Kq>SG7K3=L7X>3H6 zS;TZ(pJozKZ^DjlI-YoQ=@2LqQYa1U{qEcU#8l&^&(3Wun^mQLH_i@Y&%$Ik8ivDo z4m!@`Wv4d$8JiC~gyAzK=AvRS)V{F)+PHuJ;T|AES}OOMan&}UX5u!n1W|9EvpEsf zIWZv*$V`~b!C*~#$7?@w?BSo64fVD)W!Ij(c@lMC|A6@{{v|RTzzr8K$)zPo|7;R( zGHnT@13PY#Jaeu))r}i_v)&^)@P|#hO}rTLz3>Zi0p~4nxb02=;g8?o`qY=kCvMpGnvG9^yu&9WCjfwP6k1v;17|B z=!u~OgIZJEGNmKZ&8WCNV>blus>n)05DzxG#+y&w=C(hWs|u`$3~Tlnhx{)tRadag zaEFCQr0nSRss`y~`b=KPxFPIpOYt;xA{30|$mNnsMEmNsYIxz)!_O{aa;q;Jw!zH% zG?=igEVtj@)G6YtZg#@M90q58l+8(dtjoP25ABCZ2!EDQ8OW!3*|HTn$6lhGXk>s! zEqeAzgN4y=ro^YC;)*BBT4!IPcDgWq94fcNA>CbxA`~<^oXC9*fi0KjO% za(Di|ajTnLvXOi+JfO!tDkoYZ{;JZV9Y zEOJSNENV&GapG3glIxl2pp`1dp!5EbVQ1~>>BU`Fw=^ehEsxSDjoE7-o~oFkH>{_O zH3c5cA9jkyqLU;Q3SwJ~x>CeJQ79%by~A;Cy&_dajjsB6`_(fTz4jgKbPD@Bs(AP( z=l~Q50crx--dLIk;$LPA#^M%?7g}2Xv#Fkq)!`mihoAlW`P+^Gb8P>>sB5F6qbI#; ztRgrhQIKrn!QB$NUfLgH* z_SeFt6}Ct_gJ);dU?Q$0XM944ho*(!eMj3uQ3g@C@@ozY>N*y{ZB{Tc3D zavEdKu03CDM8R+LxSRrFQUTz}!BO8ikq3V(1{H|SfZ5|`^D4+O!!UfA-ln5Q!#^*d z2T_rhQmSWA*#^*qB9y?*<)3-mOp&NW`Hr!g&uE8~Z#2MDs2TJ(!O(b{EBk z5I$sQbJkh(r+)65^Ev^?U%<|MyjBy>oWPeK^uY8JMJ3@YHx|1mK&_1?prsZNa1J9vWs-aU2b)yd5mkA^wUx-%>d;fm<= z6r-@QM3Q34CAIQ!=v)FEx7a%Wem9T3>h3KVA4Y8D1Ja^po*7^nS~47%n&OH@E3^O<436v5hhAA zvfMT>8M4QfVIOwF6JO99B`!Ztp>9o%(NzA<|Jp19v&`->`3TqWg6h?;g(oJQ4tlXy z^D1g;#zFgNn)VT&I>gnpjT4Z_54#6OU zF_V#sEU4v?*FaV+A}D#zEP259c3re;L_mW9)mt+Po^ku%4Mz;&S`Kv6Bp#Wp)P;Wp zZpgoPAxhQ-E2G}ne4_ZTN1uQ_zh~4eN0hg5Ohi8NT}lDih+t`2lLfGK9Pyz-pg#u+ z%EzVGg)<+xeR|xg(6;@9;7ix$>N<~TIr;OkxOg{IDI2ntNn~YZ?LBYa5o7kd#1q!} zuPAPEZrgJ$CVx2^E_iB29|7>R@R$(j-8&3Stf)A@FGZ$8 z5sOe_2jH+T#Rn01m6mT64e^uf;|vla7i?@(*jzQ*%kAFn-7uQZ5{Ty))|Y0hj!Gr`uejkVhuf3hiJ%sn!$srzA?V zA#>ML4tyvv?+<$VElPJ+R|AG@Dlz7d zB8xFJy$iib74vQUgNoJEodkBp%j zWeuTP4kyk^p0$M6tBFfaxA_W4Yi|uRxZ#(}V~ua54(iqv=gt%=OIad}%ajR2! z$q5B$|L|l@Xht&7Bz7q|v%Ym}V9!r;ab1qTzTr>PtPuX+z8d`V!RAbxyh5fhq?Iv2 zl=oij6PPp?tt{nxIWMueYMFb%`VXJ{N2Zu^Xjp#yr7oEA(95Z$ z|I!caAsH&WD&w(@T0c*A4*+$2U4#b#2}J9$ZuFY*n9^S#oQw$5mhOk3HIzd;~9k#_>X*Q#RkTKbXNeAY=dc zfrQA)Apq}$bXS)Vhp3$zspH)mnJ3bii6`8ke%`Xy!#(a8 zT|-5{q97w9SFc%4L3IU(41?f)4y>B!>1t04#946{k9BmFyQATLUJJJ=KU{8TZD|Q_ z2^(o$b9J6oW}ipg<+0_s2Q;?Bu-E9fFtYRT93~E%S;u%Zase(}Vje=SjkwOOrZ!$gcjR@9EiEnYR~yVv&{$X(Lgh_+P(sBU2VzoG!r6q_ z*)bScSWp2c;l*<{vf*eNJ|hWV9#~jdS{S1|lVnR;OG-*;F%TBdG(+UbPLKlI74Drg zk&FSKsOL|=uJC{S{3^!&AvbW}mEb6H?uJl`A^sP=yN{D4H2P1M!!UE!JIPX`h5SF| zv%jt%f`vOWw|f83&bf;$r&XEp&WXv$uJbc8`ggWFJ3D(1MiRYM3RCR*elQZCWXYwg zEU&C2R=Fs5Bq$w)EX(@27zIpCO_jc~rvo6;P6i3)vh-d&SB%^ zV=S<=u)tNcp>*P9-WgEc)a0&6pJrRg{&n{&jGevx#KRtw1}2l%ZB@tT>$&$yw=j4P zbWVSENs%o^Bqd?~o1Ik^{b%;q)^Y0F*RN+fPqy-_qF=?b75-ds>W7EnusID(TJ`!9 zhG`1x6&-3B4zn2>8#~cHn3!hG{{^T_q*3V`e>pin_dngnkz z-j7pNyN6ZfKrVJ(s?@57VbWY=f8(VD9Q5wGv0@AUR@m@3usj)R0oItgI|L^NP0*CuUulP#*=JEbyClfRN9Q1-5dcEyV9)^SOWIr9FnH5H2ez?{* z7DjTn5@c-iVtH)3<3-HER@&1eTp2MG7Ll%O>-(bO-1~Cm&BN;}pSa?GI$w&(pc=x= zZOYwOURGA74?T|Rz2&(Q)C2fd+2pjGrL1c&;*$EhDgGzWW?op5mjVu(Q&G!MLxb~R zF{;3y_-RK`6#Ou~QFQKTR2Mj2iuw zt-|&8AF1!3Bzzu`wS}FXZ}iFI7f3@6sxmh9dqoPWG+>?|}k_}kc2s31v_-x(2T;1zA3q`h5rT|?Hay`P7K zgtUSUE{x5vot?_*B+z8@?iL>yIjPaEJ2Emd%u?oMB#tnqvng(y?cHw~!;q_5hAnq~ z%3ZT+Su9oD*pZZPJWf>XAcnu{=?m`KOLe+P3&0&McV~O=mERX-lxYimE^TQ z-Y(k)oxD#C%@F3A#iI~e)j)3u6vLqg2&QV4D^uL({oLuYov@9ywaTrI8@#n}y?{9ZJ`X=&YnG6_lr1xA^Ed1c~_VT{ftCnvA)1U&eo z3*XO9=uhY2)5Y@Cjq;h^1J;a-kKRCn*WGv44^{!Yqe)aWYMm;JV{D&)c5)mQOkN>r#=GFNPeaQ#W4GvTAJwxgYU) z+1t6XckN5qFCdN4vNiAB){i<(Lijz2Mzo^(*uDr0T`_m3HOqtSs#x``G!HUUeSFuySx|K1{HcHuJzNN2$3 zZ}nIWK!{3_rMw9y!#fh&TP!6eBxD_KlePr=59OEkUz&x(+GYooG?BIlr3bz}qMkWF z_x-rz`*BBS*PpXAoYDxT7hPJd!sZhGTGqA=UK9Ju`2KCN_1vq&p!dh68urZHKRyzv zHD99P6MH`?wpt?!A;@Y-s}i-)28?`1C@`S!ITl*+7MFG1$Hiv<{%_0)GfWl@6_rpW zGxzoNHLehXqXpQdDgATGBAma-u-P*IN!JFh@yEYUSI+^F%Cv(zFAoor)2l1#z1||j zDyA9>>1!TO?`_}X2SM+vcQQLKXAzfOQHYj8k1?JKfzxmvp0eR;!j2kVU2lw@$44*s zg+1Q-H9g%c<9}`@D8Dh>1P-IxP#V(8c}RY_L)H=ZZcM%pAYpJQ4c zUu|`n+r5+cpVzbn9W{)Q`UjOhWHlv+&6T%Am;J9tJFM34J!xVXaF?A-EGCjUaXLMk zZRy%$0*NF|d@t8qPSyq#+vnOdy?PW?HxVdwViYY?Hsb9Cc?7(#axllhCQno~-)V;5 z#2k(pf}$Oue5`NO=9Fvbxuk`k_(kw>jV{k;-&Hu@NsnT`T|I2BZhzr@E-~odxW@MV zv8t|j-DM9LPRO*H1^<3=Jd^*On`sX8xQn&bY@5=d*Isz#laG%?PC@-WqY9;IEC%ej zcs!DlTUmg)U{-v4Vc^D5ys*A&n5FN-l8*QHZl7Cx(tw+R>xza28YWQ^oly3y$pv_v zkGJo}0k^|Mx}i=Nuu@9!MK)bd2K38HNI#yArn042G|dS+-;kPBVGt?8r8|`eO?ip} zgkAT;NQC_sf49XZwS~AWR%%tn$(F^I0x|xBD?HTePxe2aynj4Bot=eic}NX&kn7;} z#h7l4eEYmr0Y|8h@unFO1Jr-sV7N1^M=guF*ToUcqgUT!Gy3XdC5KL&@V#bW4CLl?dei}c7v-+Tf%G&@}n zK-n!qr0S+3g(KZXAJxWkrrcKxDgpc0WPs}G_jj=KYlO!h!}38pX(1el8}>jC`wjc(AB~@-OiV5%4Rg9vcIebQkV#CRKp067`sYRmUenj5lwe0 z_xRv1)%g425F1jscB_Mz`PhZ$|0eJQ6&1DbVbgs^s~lTu69p#P(flI^y@-$CO6|D; z9Js7H9+nMCOG{}izo?^t1E6p#BAk!|hAg*?grZyF%NH-3Z&Bn!b5;)q^@^wkQAh{z zMC)JI=H^_Lw>pa5VB4{PQM7~)Pg~wQx%yNuJ^02xg{--6I6TNv`{MQt6-nfUM`hVg z$%0qyg9v72tvul9J#XHmSR*RVJZFVPnQ5;V^U&~Rv*~mQvk1@t!ty-aE}^JxQ^^+z zR9hiSx3uaApDX7W9^Y{4Lhb$Y%@1rq*}pIkVaIr^r*k;)*;bGfvk|^Mf<5REyuk8K zu{vlVBT-U>F{2PihQ;D`5Vo#uidW10zLmWS;a~r|1{JFr-sS+#4EWCft|X8MgkBw_ zwM6C|;(34gsb3s6t5p&nA(KMIu62dZE))uMZmjWZ&A4FI3~r`xaVc037;~A z70CZN99bmi(q|e~FlN9hlF{@K#YrZu`qh<$fZ!%IHFfD?qs{rU!F(i74H-L{e&R1D z+e%8D72#$6`yaY*Rrpus`9ZIzSk8RT?FAT8iz03eqxI;`iDIp%dBXnc`03%MdnvV3 zfJ;@5tHM*2HC~>l9 zxUj+!uu^uC%p+U-)f9TFlC)>9u5!CkyX+B)KWY0U+{6{q+U3)U;KxSB^Q>KA-=eKU zIwTc9K4taD1klAo`{&OI1gG7W!RH!|yE!CUBYc5u9}t z#i}Dw-qEq9S>Ig%^!#{O$zS*gx){d)9-w8dG>X-@$RXYLIlDym+z%ggaIx0()=Zu~ zSBZ^}1xsT0+7Uzfz8*zalm_`ckIVa5D)H;z6yMJ85wahKXrw|mj9O97?NUk1Y*)9i zdqC*{8zz>F#JtjZ&o)^k^v#3$dG!T{IS!(rOFf2Wi zh(r@EzJ5;z5vQvY1fHn_KI)5t_Ge5KrP_No+2GB-RcQU0pCXpT+`=tnfNNBSL>>f! zI&EiZb_dbFF+2A>q`Bs^js!1KdsoW>bK|=qQBdK%2^br2M)o0qaITWa%oX&9lnFjg zH`P8Vt{&flwvP2tvvY;YD)?FT-aw@|pwZsIH!sG%f8$Jbi&Oy6hx#s5K@Fwd+nSlw zt?;#PV#f_L4l)^2Og$P!Og#pE@$rvFAyINu0h2#)nihLl#r$~ivN>R~gmvEr-q>rT z@-hH-eHZMewevBzRQjjlRL?XFC~5Kpfjm4smt?QAkwHc(pG1KS@7uVl{d=L7{te_Y zC!QfixdT$J5Tg+VS!6eAAYw#Q9_Ta=CmCVfB1ZM_R#98s<^Nj&@jFm_xQCCK5?W6)zvJWToS^wE};_h zR@>Xy_~<=FPlND#bV&M`MS?ujxvPGC(6KCzcNf`YTUik647C!}oo`(SwE-N}ablr2 zy%*WR{%jj#1{osHam8@OQUltDU{%!(vJnf8TX4AUs*&}@=GqJ74|sM#t`i$pFfA0#1moo}h77*xsFeS5l2k0<79^wP5+Um|nTq+Q1$_4(HVa>jBy zfn+3AjVPwVyH}5B6ehk203&F*`G?3bKAw|;2!0K*!f zmQDS2;0Z;{TBoX-g6K~935rm@-;8b?T1L~BQ{mh9v0c7h!nw$86cq_1>`n7#nW<&B zQER+nMcvULd`=dcWmWN&TrjlH;L&xZRTYTf_u~=e0j(@7G#w3+ecp@|2scJCm$AQc zJlDf1d_(naPRpK>sP|3N*Vi|NxhN0T(pA)<;V(2ejJ(~$bFVcUJ|#x% zTN}(43V@b$zB|5;ztF~<?+AN?uxbaMC|%e_@ftx7x=R{!*{ip z$t05U;G&^G|0I>3ErAVs4;a!$a*G!G^$x2|mG@mR%BQeh>#{5U317IiKy~CT&J9yM zV!Zd#9Bg>PR3tjspn7bxbCle^)FO8OI&qps ztqn2W(YBe{`MlgQUOb3m9Z=A>r*j3Bx^eOyYvww!>xKQ?U$&omt-~qe>Qa2ibEA)C8ucygnOTPpWuw)RYr7Eq zfPvdjU;5AwshWp!qkg{VpGH+sJQTkQ_SQ7LT4Hv_7@(LA;WGCf=lMHn+gqpTD>i}j zhT$8hg>aROHMARhv9zo=s1j?;0Ww_o$ie^5-R z2Fo=AjG_imjzf*9RR<-H5iq21=xsou`Z#uF;6x~S?Gwz|CHY_X@UhwN(W|y^FGz&v)-WYV2q`}9-Vyu6T<%H}bbbQ?Bc?r;`J(Bok z>S%fQt47iVRkdrn^x80aXE$?Lc!g6E3DRQSZqFeM{gAMl&BuSY$Rpj}r;Wnq=B9uX z88Z+}cqq~T=fU-eg(XZ}4c~tg`pAG2o9LX8Iwkm_mzU#wzR z>B*X1?+xPe$aQ$xF43yH#GynDsGf-6Tjk0jVlEGJx-AcD1bamDjd9)b*pmLnjsFIV z)rJuChA4`v`M8cf=58TS>1PXE;b8pGV1z5^;yU!#fc(%P{8Oc8IS@9+9#&!Di$b%b zFL@RO-hSfyXkx~UQ(N^HG5=bkV|i;1V~^x-)Ng&i#UZZwHnTHKJ}3r;dUt&IO!p{g zBfJT7Qwz+H=OhpDfnhfY-QrhIJIJ;8x*>Y1HG5r7$Zf zAPh!?(W%j+Cs7RyMz`}gB0@+Ojk=$B!hc7pWdF8T|9+gnmCnZlF8SSEFJ&7iJKe`Q zyh#iXLNOs<8vJcB8Ip+2APYja69q$LF7(XH=u)7))uNArKW;u*=F zv1`tqFJ|`e#)Yq$vnNq^?Y`a}_~{H7^`5ZQjZe?rt~ArFNt;2pDJ1!;AF&3Cap6+c z5W8f||Bfb9-7ejO57>~&N-NgpwXf=#%co;84znq&Ybk6gP6d1dQq%tn?Z z%N>|@;kqKIj?_O)FV&VLgL}(UbG4M7~ry=o;<_GWgT{~BUxrkg!vti zz_Q=1hAmkR5#v!Q?&QHLqZ*w{$7Uq-$25Wa0LeiKG+A`O9w`zV1^qFxz8Ky-z)W_HWyP z*?^eVSeM*=HpU*W*@u_NI}@X9ewXn#ik;Js+rg4npj*m+$_t2I=LvafV)i2jD)3A+LC@UewK-2-T(w{)RunaPVmEx}wpW?17r~?jd#@fy9!i*Ep>s z-44vWLNl$x9Ly5j4dMd9FE(D`PsDkR4otARGkH{xv2GyRYQyQSgb&~ZtSOLZpryX& zmf4yx)IQclpVuf-Ke0rIpis(BX{RPXseX<@N0dNeao9Um#AT^fAYubHNoj+qIx%t) zgmdfa~)Z8<#87_6I42hvf3J89J4QJRE(3Dq$ zY(jLy`0}zwh19vDeg)}}l=1VUa5-f5=<4M!es8t-ALa;bbywy{hT` zDWcX1Z1?4Sse)J={W_^(BOxy2Hw3(-n$$zCM;h_&Yp}+`T?8vONiVfAU!bi_Bk)sA*mi zrN8)_&fMnu31QgI24;dYv4YtfTTH3`Z-$0(Uf+GJuiFpMeDCLE;yGNv^hY))#fmr1 zF^oO7S(Nt|V=C^ac9NSsG1Xn5hA-|K#z>cTzCfKqMm00bys%N6-qs9HLH_z=8p@sv_Vh&h% z*xMfyml%T)KC_(U8@k8p!RI0FA+#mu+FV~|v$%l-i|6iDcABNrXa74lD0X5ym2?fg z<>l7e_%8JX$4ra$B8EQ^gpRw2m(1#*jlC4Fb}a1kugdV8Zh-xvIp<$j*Q*1AZ;DLa z$OJW{je3LcMikJW^jVD%li#792w8s^tQpvCHqaw8YhAYo`_#?**0Z+7a=!)nVV~k5 z1W!|5N;tcjbY*lAAO&3r@mwwp#oMjm?^mL)p0Bit&& zo2z!veC}+L%jt)<@lR!=-h>L@v7zg7=)K(B@F`auxowS}^yU~)NPK3N@n$o0adu5y zKwNff*t{MyL;Cgf(U&u}k@qV9a~w(BVQt9Ol*Rv`YL+_exj=1}Q4a#y4yD*kyD}z- z2kpuzEtVLUl}pt|JEY~TdqKQyqB${~)SAZpyAMv*#L~n!J!2OYSLeCqw$-a^v)5YqwPCrx!fUB1u5?2IfLbm-nlC)f}z+??^R&RDXdC z<&jQC%QgusqdQTKH5xwr7R5vXS$H`&hC0XLFG_*Hb~oOIxrqrmM2(%3 z)AX9%q!s~3mb9Lc(H3b&rB%cPZVWHYn$Sf(jw#1{y!;W=3|1a4x00AYi+;jO?BxzyexT>dYz4UC&wqwmfFA{DcAy!Ch3 zsbrF|wQr?mj?tYhynY9<%PWFp&q)Z?oVfWGv5vZ^ZH!o898U99RTSF93#y)Tz(2v8 zBv;uaK=nybmmX1qLajL)kFoH%9k6^3K=+;+9gUjVV5Q3zv#J?J zcuxN1(7+*cfAO z1HR{-R`47l#ImbF`l$Aj?*kZ2r?UT{j;!5t!u9U*sp%T-lgc(&2;6ui*i(`qx8d;}?=weZek&(lL68yZ=Ck zJm81lbjl5P-VSf80wVpGt!|s!=p21#ouckQD6VU5OVp?dVLmfg{fet!I-zJ?u6QC!Y zA;9o^d32D`u7+J@Ay<=^4Z2#&3O9Lp=xx&%*`60!U!OkI0#IL^L~h7Q2If8T6d%Z6 zw3^lnjFO=R5Y3tKkWZsq0ea_8Am_$g$c>~TJ=LVrFjyXnaA+Y>_Ohc!f*LsAt!E5r zn>0HeW&QM&(=stH1oLz7eD9nqkI_=$ew*&>(DlItYrbJEp9O?>gc_M~TnNSUMMYUD z!x<^;XONVBp{WRAeoKPcVl)(SPGs!hvYcc@rnt4Roq0lQSAP0?I_J(**?k3qOAokvGU!}uU@A&e+LN$t{e(G2PAzSgOSN#K3L zKp7EO0ceEHf-Gjt#xVREi)pNElTPFr%No}k6NBV2svrAv(wj3gSZ?we%;JOH0MDGa zWP!4UAP`)_gGWTGN4sm`*n0{|5yz8$AphXCA^|Q^nv`jkPSuHZh{IfmMNMHI(JVX?;$O7R7WnxpbGZxXor$?DxD{vHT z2$3S}k31`0u}&iRfBKDoh__tXH&8U$Ix+^qFCyyDtr=SUbjI~Q#&Cn#}4I1av;DHZ&}6ATm7TK)iSi~z{t;4lInU}8!NL;9b3 zi}|F1X_J#75*Ju^yeWm@NJ9CA>-^%`Vd-7FalE+y={BO)%?bR|{^Ot_;~DC+D5xEQ z;zh#nU%X7D1tTv-l1aEERB2${am~eW2k}D0$rLmI>!7@M2w|U3f4$;YIFa8&(j)On3ho zS>pbugke!4q`n%k&hTJaWmy{XmVHArsbDU`HXv*a6YF|v7Uz~ZeRYVlY^!$hn=UUv zG2x3KnjT2fdj*lO;xvJFaY+S9wBGkyYqmMfmE1-1q zzdl}yWX`+`D^{Q46CFfrhJ&^`-w$qsQITx2@XD%DToy?&3Rk z7muK21x(oKGE&BK@^8~Xrx>Y!ZG~NK5Z^3o{_>R~vjJ~qb@7*PKo%44NwaanV|T@j zRhvjT6_$4TnS7<0A<0+T<482(iB^DFO?6GL{12(1Ha#|eQ^n~0QdZx609}d3FlLbt z*bFRZ@`THRJ2!P1^o%3u{45zc80dLvi++}oqrp;gvdk?}3ZY9PRB=Jm%Jx&{`PKoqIY@wXFFBpJ z@aMcQ0UNh%Wr$U~n9X^*LWz#J28xh;Js-XTTsLW&RT$2-ea3iHSMfj`+@H~_75W4$ z>O|Ff4MJso$SZJko0QoWN6{*R;%eu8?9T~$>Xk*PrXEQ4ay5ybGG(t1d5VZaHqiPF z1bHW1W?w7vKPnJTzG z^VR+tl5D{^El)q_Lh4GGb&=+?>09n6%_eK`q8&(fw*kRGZ>rd#($y7Ly1I56wI3AE z1eRvgi!vgqG3@8o>4z{f1goC}Dq%lvw{~ENCHV zrY=9Yq+ZnGKNKm{Hy-6JbXuFEK(H0cz2g~r?&x8s3R@G9#-8OFaEhc!tf6$$3f-BA z4d7u#A>)BHbd)nV6KX&?4pd8A;_@SvxQaT>T8osQh|=d}i*`r-aMQZ$x));9*uYbt zeEgfC$B^`=4e*@dftJ50zi!K4-lDD!z)fZ4+VgAGcH#w#vhH7Uy-S2NtB!rfG;x>) zAM}r;sdpF%Og#)@R0!2B*o%BFTQG(LafSn=u9+n3v}?%e(r{CJm@Av0%iu2W1aGU8 zMKe1yrKe~y5QoS=J-2IgW!9KLnsi);C`N{P;M{yaJr}%C8 zfu`l#^c&T1-*`P+A{IFX1aDeN&V8k}%!NUnEGzc-_kz$M-iquFO3hrM%Fk~J?bSP) zybh*`;{(CkHYUTIy*4SD)Q7h0JJ{{$8X)^Qj(|yvLhXoiO_;Wj5^-_iOKV#*2ztNatRxZ*58DoNC%(g@Q z4Rkukady+Cl`947HKZIDCnK}#YMYgiPkF4iTwA<$*MAyZyF9rQ-7zj@tQfDU|M+=t zmvgSWQ*}0tJkfc!x@t? z^8Q4Tk*iQ>Gptm|vJ_FY?2k(6@~#YH@WEz%$=z!RZRec$Pqllyt3)s= zD&ECLixI{qX2lq0>W4|3QC`kwIMsF5D?At zTz+vZRb|IB2Umg$g=d%@OHswFh~hjMIc8a@;@stibzW`Ht*)r0_^1*BuyJsp&C?gd z7iwS|ou!EckrHoLUCw$1S(g~7u%g`e4y86Ri%t}L)!cohv22G}JE+o!k1Rq#)_TRQ z4UZ>(f^)Q(c$4-BL~z$S|F)K}PanqIXA)dkIBb$&|7c*9tkF4$5y7_h+z)xwyuoxl zsHA+@*WqK|57A=u7!7Tfu~Ut0Q#P6j>`Y7o7@K$SbI2UG4dnWH?fz<@WTFD|?U#$= zi2G*Q=a1R)$eW$ecEafKPx2f;%lC7PnRLT_(4dKkBQ5#4*UXu#AIDkjGV4<6`#ipG zXE=PC=mE6(t^~~rQixiEtsyzA^y730aNM?px9k{1oS272MWt@f><{f?%myIRH%s>x zj_x|wtKx{UJt7h(0H4qxvk)x?IUTK3kO1$}cY9|3?h<;{di2HqB!OsrUq;^3A+opc zEBJoQlv;1N4ay>2m<^zY|rllNrvAnu+lFtOhtSey6gx_d;$`uOr8h zitF>-A96iCIbpv8m*ca&)t1rYVbQY!iSDxG6)PZ6vP{0ozeQUpVzxL$mV{Th%8dMV zwSGm-Jf zWvZA#V7q8BzxD(mqk%Y0dDFYZEj}kn%We#o+M-&wT~Mjlu@Nfx zLYf=*7H^oNr%YMmFQT4NhmutN*O#9T(D)V<+S%S!a&>Q~y?je&K~uWX;FF*agw=^^ zf9bcxe563$ea1^gE-mrWOCd8_5NQnY{7gl#|FSmO{%CWij#tTni|X8zk(zj?s2O&q zM65u^)S@P#N0WU0r-uob&Dg*`d(-Ruk`~0z&R*~%pF45&Xv%;g0Tyc0Z8Rp#k{6el zgH;`8HmXm|$0briUqPzVC)$>hJ#gl2$q_eRSw2uIn;{@VtKdelz*U%80o#7^uh1`t z(2`U`E-x37p=01#a@^bg{zT_PXo^-W%I4a$qBECG8KwqudxKfc*sie0Dd8!>S0`qz zA!BQA=U^phX&Zl;nQRn&Yv;~y(L`-ryrG8zq{K?B*Z{4z?@A>CE!nj3VllRP^E3x% zBFZ*2EmAd$5r3DCNPkyYu4(oTG{;KbWfVE~RIF5JAJjZx=gteeZ43RNPalU$B}x&t z#2VLcPM*_joo+v{_O5De%S_~r?PDcXD4StJmR`3 zy=8u^D8{?BJ*UTe8Fa57ho+Szy;g~LcJxY|b9Ip%|1UR`oogXsa&2q8ur2S-PV9h) z+#F}Eq+TRTizG?Pujn6Yrsk8oyWSBk6~{-UMW*C9CC-8-bpBo*Y#A}95ebT<`mK>% zZG2b~tta}|DG^XyselMi(eiBp19L;&Su$J-1&TcwMfbx(W^s`hE`**ujdIavcj)LMOyxzAa~K@SsV3v zE-%yNw-i~cX*8L)MMfdBtiBf(n?1bn<&d#UtJCoD_t=H94VYak>98+lA{`0-Y z5zUsQ>!g3RR)nI+hSwZwGGS+gipomm1NweQRaL9~X4U-fLl%{^Wsbo6KHt}WT(^V`nNbN|(PlhepIFRH4%vto#|x;)}?xbI~G7(hp!J&v{{_is{Q5%(So!C$luxQH_Gf8Yf$1KiClqch*8USX`HF0lxyhtr)7 z{$S<10*1{}j@Blri#{zGdT z*=Kpa|9hsvhOc9)(Wo1NO*u3O|C6R?gksE)99XLSP|~PAchu5jCfKB|L!?czQprD# z;u-*Cj^s}cpeLl4}xexwy-?vy$=j;*MfD%kHRc5`M z!73X(#B3qqs)^yvJo)LV^l;U@oJBPnvUp55F`p2h{wdmSIJzkXHDxFslVRnnl?upM zH7}}av=f%`pbpM+I#cM%aXOGxEZ;Y85Ig9Uels&K#c!aW4u6PvR`T|@S2XmtaO-c^ zGT=@?_CEV%EW@pbB=6;Dl1_SjDMnkFlFFz|hdC*4lsSnpV&KG8#Lv%7&(`xC+T7YK zCnW`20}_HB=2_>Y41o?G4Ldy?=Cl98y^H>0jYrZ%tf(Rbaf`?yHsn6O9cZqME8JL$m;;q4`~$1M}`hU?1H?w-xaYZFfW)|x#W!;r4%zkzi7 z7Rbn-Tb6$C^B zCB;AY^WPy9pRI4Zes@n*fq;xwT4?yw7(+&R1BVjzKWfIg!RfFD9F865FpeA&E-s^3 zm1H~*%{r)`FeG7cv&)#~hONJyy`=5*Ow%!pGw>GlKIJcGc)XFc3B(wWD)v`sA=SLZ!cr;sI>_AV=7hqr>QJNk z&Y(L^yN!7%mUvRpCHKia9kaP+`TJz;vHZ!y;i~WdH597*^=qU|qS9eScTK+nKNp~a z{mNAgnW}z2Dq;B2JGsu{cAe$saoVMg)rKs(x=o?Q*HC+&%)J4rk+6?}!=7Rc)819k z-1Mv0YCyE6PO>%7t6%Qki+`=qDR?zeYr893|7 ziBDr)AC-Xg(;l9l6!i2mJFM?QKK4Jgb#$@=Q#uBE zGGY}|@f26_CR|l6JDH>K6FO>NoxzY2%rTYrI;6rt(@2w#6jO2XuTd=}Nk_wsU*KV> zl;A7I=A#Ya;DD&Xl1LeQtLUt1f1XI7zP^D$JbTfN^gw+28(1VSQ}xtPSEmHG0hIOk z_}ms%gW)5PAH7|>!o|bWvy_o!z+~3&TNH7vt&B|Mi-EpABe?UjsaEhx!_l#(;|D&g z;jgkXT3ZK)-(zEP8gjRI{q(;Z*ffp({QM>#aX=j*_5U>wUOvFImw|h2kM#9j@^6gy zNX-Qe4^-(bwhO^Bz;D^)%_va$pUqt{+|KqmDR@f{JK2b`SL$74dA$lShggtv<*&iJ zWW+*KdI+UPl5>&-PP#a?rGW!jYr6jN&w;;T`m27ygf|7AMn*Y zXk;MK>QP`Jkl+pMmD^A=QNz%1(G0C|ep}Dv@r2X%n*Z&T<3N8JH@0?&wKvkxNNH5Q z;B8OL&CLb)dJ(@H_4s+(lePfRQr*EKY7m;(;BSM5b&m z#c1Ggm1E4Wc8Stqt1K8rAO%&$KcisYDHu?-E5davLj_Ws>A$ow8dkeTkKI`0FQ&K3 zS(cbCi%5L3OU=R7Nn)J`H8}~j86c#=BAExjJjda$*Ct*+BUgb?vBi$cYig!+*Xk$L z)^xkPYBJFW20djU?FSyhEDhg8~UiJu2yu6w5FO|)nu89M}wye^AN?m60na0WN~9~$wfYH$1dgE6@$M|%1_r6`bH zF83IRA6lP)pjysL;#B;L6@9Mux;SSR6#v}Y_x!J(aO?X^@{fWd|&LbP8b zMrbfsVtQxlo<;!~VLT!oEuYe!gOt28-sY^PiaP5!e{h>%nUUte9a?wdT}Bo35I3O) z$k5sd#tw^7n8bsx6(2?HCL!6v?b=afzw#RerYG9+ux~2*m~AG*AxW zGG%dE!J(`A@6Q)47h{{d>+9^$wHzV`q-zAM+wca3&8)7}+@^%g5}IDYJ>S~PZ?I_}+$E^gaczGuX$ewau%%+2^3izO57Ppej4U07IdNw$9n&}HHCt|S*xVu}Qy+$1pf#!Q~yn27FPIvQjZUy27etU1P zh$U`k)9DS6>IE(h?Wp)yL@IPO`*eDTe2zV0l0-G)m6{R&ko~j7#&9fh_P2f1W+^HI36yXi`G!O&=SK=JBGP=$j+XYD+ zS@~3|fDv$3JHoJ?4X^+1r+w6UTo{TbKl8G@SmA|N$AJ2X5JFB( z^RAzHP^a~RRo;47-qn>8Rd2nUtkz%dDei8;3xyYVcXyZ0;eR!A zGoM^>k>uo%WBa%ETF)-6;?Q*12$zGMr!kmL;5p{>d^lPH_|2j%K3)E*t@9>aDi&OH`2d$jR*O#It$ z1o8>z7w3>qQGWKNv!hMcP4;~`yb^$f}nL4sc^RwLP1n9j$vsm>Og z3k$x8_DAFWplb0-t>DM-$NoEQ$GKkSHVo%@-88`p9x%H1&TP4!)s7Sc&%6SYH12H| zI^zja2OpYk5JT24V~YY6I$K5s*!Ae%KyIWVVHd_12QyQH0(y-Q*MeQ2dMZUtZoPU} zynf}B^S!F9V{rzvWQ3XqPaLTjd0zB<w=Ktv1IJ_x8P&6xvtt5%RcnS2uad`w9+Ae_{^cgV5EI4?v6e=e?pQ?jHAi&t z4)Pe^k3dNHN&o$?tTnN@9nzV?W6w%mYf3IE>(03kuCklZcBXUWYU#yNtO{2Ln`kIJ z3ZiAcP*^7&;lt8-7bG6F>$<1* z$B9q4w<$S!=Kv!6k0F7y=uIkzG6?&-aUyxPbxglK-EnG#a%y$rgRliVbMN1u`=`pu zqCYC{uq|)`-Gphz;e>c{^n8@Xs-?#Z;5Nt6VDyQgYX7E(%*C_7E!Pr$mH^y9Vj6~y ziC<7N;F+)%ci58VljBu}VuCoM{%N)#j05rd=eIhmc7df5>ouH4JBuch{s6xUj|ha& z@sP)D&**i(nef(ztXT5MGE7L&&=2?1u=V;}2?L`Bl>d?p!JBo@h81xjI(+RlF>+XD(0Uq-5$~L8% zP+M(%Qh&x#tBg?GY^;IMWkx*kAdDm$d$@Xq*uvdamFRDph^Jc1$FV=D4~K;un)Tj~ z28b0sMvv4TbCOSD{bS;5zQLfOv1`>8KF zp*(o3JoEt?B)~g!jh&B=kZ(q_Q$7nkPVvU|<<(?a9@R9ML4|nl!$4qdHuh>i!+>hT znJw}-`%kE}(Qk&w3!xuYU(q!G-=&GEipiDmE3ZbK!yHf6h@UJwVqo)>0T0c+lDhz2 zILUbD)!qTmtf6gd>g|1ZbiO$d$W4GA$|Du(lK*o5{e=@B*k*ym5KD|mtdf!v#q`j~ zugc2!{hahNZ|f=G_q$Ms&^9(N@U#$GhWF||nJH@`!u=CTQB|1iA1NJ2ZrtU{dAu|+dLB0RLqju|u}Wui24 zXWq+Alm-!H3dmVqVu@J;d>iIA@*bC@#2Ha4QAWcn#{VUzWWZ+_ZKPYl(w(1INjG_- z*5$79!h>nct5pGX!Ir{W{G)P)=(~4}Z|bR@ck$m(AY2s{3_@pPIvW6OoDKTL0x zxO_?RBbyjZBQ<9Lm{39r26Ws8EI8SvvtQ&e1bgL;^UOh-qf5@m(!zY8ZJd zHIeJFu|oj=(S3+~M`Wh7ECJ_8>`YK4#qgRHt(EV*6pU8_@T0T@}e()e`lv{;P&_3>J+R&+33ICy*(C% zxC!tVADRe_Y5rgow%(H?}#;)iBYTPG4F2t>Atiru%`dvcOmaKD%9T{g7TUxPkng!;^z2C z{J9RmPZZUsa+e#lYFBhT9Mr+{h>1Dg2&Pq;`rcl{T7FgOw|S3K5Suzw{tLq*SBJN% zq^y|K`1HXdfV=`f;8M@2-2$j=7FRuiRC3`cZYSw11^9Q$+>~GI+oMe&aOH{|rTpW4 zoX5+P4=E`r&tWd_^wJW?^z<~rM{trkfowfG3`pkKO~W_9zhy51if8#;xb^Q*@#H+1 zND5jlT(d2g-P%evCAbFl5@zQnk%)?aUARyPf?1Z-es(-Ap9+LhN=Sx*iIW#EytfiR z#;ld;HL5HuF2-_$In?HU2)oHTc0PNSl$K^%{58%7f@qQeaLF2d!#$KSF7&b*;bV_T zPjmJM-x*P62L}gNAXHm~NO8D$sr>QNr}(z>e)6KPo_g(b;?hWuOCA2L8ME{AtZzG? zYBF{kJgX?^f{tlNgo+lw!`@lj*kriqkA&~7&s7C46MZ9x7^QI;DEOY-9Yc^Y7C2+V zu_=>)+}*N1wZ2srah%K5o|>@VU6p>|^Yu=a^HVt0m%*pL`hnAj*J#d(o0(ZYF1K%d zb=(KwZZ>a~lLD-VYMquUpC50FEBHn+20Z`$aN8Xnva4*jtPm`-(WG%!iePM*jdK2d zAt}cLRPkzOxw4VOD>GmWi-fhvUo)v%b-_V+gk^>DBUpD}T{)y{p=4gMcMOTD%^8iL zBae?JyPR#4%gJ$Z*nG!x)l!a4>tUL2BEiI|i;8dC%{fRRZ}$fUv~6~!;Ec1uS!*pl zUP_HiuETAQRRcZ3FgDFkeVBr@(hqm%nJz{1m{p)p2zwz<1&)_0)XzmwopQ?mGQAgpEze+crrkAZIQ!GguB0I6U z@WLG_x&%49RBq&{^Q@1gWXvPVc}OVyFv2a~3^5{pPB~~Kjbk$|#b>s$LLG=FC^_Ut z!IOjQ0REH?n19v@YHb{N;&YdMa=gyl8({Lh)+i}Qi(FN-13!G8S?wq?(d>QfF!}^ciC8I6PyB(@GhKA$SDX{hdy*q$z^Y4sm92gOLxZ zd1UPpVZ5>yZ%&Q?99FtncT)fLT%C=^LKGHc^!rfe7@7973M857|UgV z$A%}#w73bXCWA|wpGJPFhZ-aySCnTKhcTO@7fTWPt)5A>iq;1njlT+_I@gm|b!Z}t zE<*5KV7$M@m{x+NPK`!NS{1bjpNnIF=ZHt`jE)J3xJWP=58isWP)~qbNbu;^|I~Li z>oAuloMaB3ayQ;Bq!5vqL;trn$FhVpXfniNOPT2+I0|{GTY#NFqRLo)e$^a5hNkL- zL!AO)BjT6`2Ntnu;=4o-@oRbkNuy>jUfbapIk!6>J19|KI>*zr{VMRT4A<*Df245~ zm^r$>S@WL5DO2DGvEEqO6v=}RJwzNH|A9fv>1e|)Bs*K28>H_FB99=E-a8+lP6Oi1 zqrJJ7S=mIkEatR78=R&38G8{z>B^8IKzOdkZx-&-T4{P;O!g)86E!!G4)av&^TIn> z?r<+dv=F=~USJj&(eJWLy&k^YDGM+(H16WKz^c!^Zcmn3$NPCvd3W-S!&zvWQRKy% z#F~iqO*4LpE`2#HcU|iW(m7Xn^o1KGo~dulds9lmrIAUBo(M(NxnAznLT`hmiv2=%tXZu?*7tQn@-?tGWv70*A!pIsk7SeHs2?J*< zjhw!VtTpz)L-*G7$IB3B(etd#zNUvmNuRXsF>vd+H0*IoabZZqIFGOokE324W^4jY zD>IDP7^H^*hkC2Lm(GD|iB-2_zT)kd4x;0h9Sb+PK&=raO@bfkJ#hf>hGF4Zjbc-L z`{SI=uVpXc1%OXKQgi%-Xdi+JXuQ1-^Tm@aOo9@PQarU=uXd^PmvdK=AMgNv9n-VA%0b{28P zyV3auocdX;RK6r%Ro3bX)l4C?K;jB=JbULKRbejK)mUsSLgK*LUsdic0jUnuWHavG zoXgOkb)L-2gPL`1gwo{sgn9UyiRn*P0k7-pSr#Rk770PeQ6Z+?v3Ja-X08qYI7zei zQr9C5gN|o4s{s{M6!FV4oygxFY(}pxQXRluFp;1$pPZ*)6lbjQkT$wv0z3 z2pw0lJHBGjGRxOh{KQaef^oPhbih*8lFiw{6h}Yh8}Z| zTD9n~G*F8Mw(i*IW1`GmZ}o6!2};)o5RgT#L72w3_a^V_R-s6{NS%w6MpuR?z7%#t z-3K9xM6ifBEaQy)Op&HoHPWxsR_t&qkNY32ye8u6_a+^ZPkd>ro^%$WWeicjn`OBm zb^t98aNLx<)hn!+YAo!in?^O4|246-afjDYFv4lTWW-bo`bqOK*J{}}JzhE~-fxTE z?PP2@?r$q*C_U7;f(ve$GQxLk=$SH<@CIL=D;o9#rN)Kx0LX)ezPy74_}wvz2Ef?LdY zC?QnNdVj(iXT~YiG%>ye^Dk4Hg?|icT|OD}WJusdvn))a3x}n@DV93><c-+h!V1p;U*|#E4hyRC4JV z1X*HizLs7-b&93Bmih$ z7~wFXXlRT4CX+d!VY8*+nmay>q{lY=>i*iRynPtc>fxO?#z7CP^hs1ncHvfjHGen1 zt4Ih1R`TZiKek?)WEr8LnkRyluhEK#_B|1#bLkFkfa;Tw+b4uGLv{5C zuE}-m+(!qx;&SU?lPu!Tr>q@o?(lJg!4C?rAFas+Hb%!9ykc!yqu78HSNE#ZSg13= z((R8pE*tr;{l$Yj@_ zp{0dpcfF#A%)L2hiUvBF=Iq`ejeb@@7~57SfLRZi^ftwGaMUPBy+qiTwafPB(_f0n zhqQE7q1!a1yPfR@%(ojPSWGgK0^43<#WsPq@8QVl%dlqd5IO%q_M>IRm>uang_?z0 z;#={%D0~S2DSj(y+@4?B`ceP(rZX}(vMpq0LDG6M7BSJW7vud6ovfFfU=lK*gs{nM zDqz`BIQ!Yk;n|j3Oq#{wws2t9=!a!R^)dfV&}yy=g<6~Vi7AmZ3}P=sB`tWmIAx2z zqMEYMj|^C!lCZ8|%h_=%V%9`^w9$p<#>&dds^{>0!$U^whx(7RF+vH*W$0tx(8~qX z*JvsM*wiuRpDk4hO~@^pZtQy2l|G2!TQ zrb(jsSnWRF%R`pKUEL>F1>}^Lf89FL0)1JGadgJiZeHkMbCBH>xmR7+-zZ6MT3D4V z&&3CW5@2r%_sH&gY^6=a3CWeCHYcwA(d8rin79*CzCqd4=d07&R|_EXcg6q~^d6fw zWZNpp82)|Dha?-CfGQ@=IV!p++!GT@uyM6flO-B$0SW7$A5L_V9-sRC*y8Y+^a!~| z`zXMFhQwks`v1+@$yZGcz#x_d(S|v9ntp(M&G@PcMob(! zsArqBXu|``w^=sv8eOe!hVknl*d5r(be02k955u$rL#%b zizgPL|LiRf()D2TKaB|1-hs+AehLn?@xDR>idohh47>jT;#w~bopo9d8}!<0sn?bf zEJ|w&lJ!H*kRG+Hp*u$gghK*8ew1EZIq|ahI?Z#Qk#KbBLm$wiakLBG&sYZyPjOZe zp|WqpHQNI@z)(QWrEhK#PZRLvGe~phi~FIEtOg~2H|yWypa4j-Fd-E7(u`n10in-) zQSQ&bMrRujzzx8~1BUj96431T+KUD04GI8~B=eSLx*DKRLkUDWZ+*cB4aS ztcuEik&(w&4ETVKc`S2P^5^loB~>%^dxvE1gSEFx(5o7Y_Q&aJG*m2;=j$6;l8gIs z#F5lvh~51k=fzvTA|-k(9qj_{4cK~K?m!A)&^(NaKHwX!M%g?@G}{^f=O8@L|d0Fv(Y{gSnujAEG#Gm=A>KG0_V?5 zP@*rOl$k%?WcR1|Y;2(kyWUla{(HJ)%VK{k-H->x<(dGHUw?d8E~#vsoCKFM)n56M z|0M6@(5N~c^cshXVl$`6n-sArglJwx2;7kr^_5Cg6KRJxTobR@j~cI2!%c7@4e(_q z!55TaV@1lD<^b!osB-5cwf|HMMk^i!7uNLU+P3+$<8cJ3W)lUlEV0@jC42#7mT0uh z4^L-3uE4Unc|zaCM8DQD*6%8x`dtrOGs@8Gk)-HqM?b`M!0f#L>tyKS(w3(H6{3^I z%_t_20XAd!^9`Ufy8e5<9R`@9TMDi+Q>Z*L=B&#;yV?Ef=Ayu%M6LT-SlDNcz`vmo zdsbTpvql2F3jq{9tp=x66~)(MjcyGCaCKWZi%x<$l~5tBQnq>O2;@h>Wp{k?1EArM zfmFBtN(}~BaQe9M8#S4BgCfg~POEJO7r^FO4gYK0e|dhalv50~AnrG4^&A3R{QF>F z=aX=)5n&Dh6Eu0-a(-&4MGaU8j5S07Hm)5c2a4OHd9$UGVNyGN?OLq%#h=I$faeRu zOZ^`@!v@E#wSpUZ{+J{Y!jr^RNEC7w@F6*YZ)s`d$B@x8y`nnR|LLCenTBiq*AS7P z81T+w*gOcqTGBW(GjrZDkpR=PO&GJZHc8@5a4%AaT>+@dfjsTfXOtl(63BhmGOJ|W z(fZ#@rRTm~kbS27g+m2raG+$F965+JJ*Q3%*(bLm2*>~T@ZoTI& zA|oR+7tSaRZEq#>n|C5MRBiG?Wkoy_&Bwh^Af&=xmPL=;#>V2%o8|L-;sm3=5gh|? z89Ny>-cM1k#jtHnEiD-n2BaQW9No?`|C)0d#!x8X&lJ?vcVHsy_w0;Yyg%V^)xEi8 zz5$2BMe_Am1naB5-WWi{{}a&dMS8ua|9@XfEa$E$bMXM~KM9Pc66i zfJRk>sfCP;j4xmcqr@}IuX54?`Y1?%GeWP{^6&3<-`jxq6xf0To9C~Z`cg$9yF;BT z@D6hxpiLDL60#1!Dh1b7Cht?DL zdIAyBTJ<#FUh~&2Fm`$VpPBwx);d1n4+ScQzr7F4IV1=Eo2Q~RC{M*dpgWPBhxe&h zIu%0k-0@W<0PPNpceUxNP6J%-IY8zO`XsZN%u7n1EKSEXEThy%eDR(!nJtDE?VcX* ztnz6%%JJpDW9pU%jPM>jOv4vpvmAVU{34^}z6Id^#36{J<|;{0tO;T^+jQNIDCPbn z$Irvas9r=znmTOa=eHE0)bQ=wH#r78o*Y_FP_dFkRn=%(=aYrc{dU^66!k;G5`d9* zLnoaXgRY(mPHt@#JPqYMMN){k%LSaF1gbg$l=>;L-32kqAPHmHH0b3slb{gOXgCN= z1YW_|`~#99wk?nQ__08`@(A3AOLz946b|WnRG%qpP*vcvRNy+F^x9<3OI;hl7Z*Hi zyW5DCJzF|7=`grO30OlZXl~X4=+Tt-FSng<$p!2ayxI>9NJDYqp8=-h=dkc_E6$!5 zpp#%b4+yrxvw+}j@Y;K5-Y?1oz!ZQX?f!Mm{?dXp>>F^h>bB~CE3c)Ml7Z6w3=r{{ z0i0h-bJ&O4#g5$%r-_vBJyj{tXffzcLtWSes$HddLR$#5%APxZMv8MmFD_hx`55oC zlL%uQR|E8rk&$C8ngAJ=)O44K2o;q>u<;i>D#guX5RM;BeZ}&C{?+yM(CN#`slw;a zvm-_sOD|S|H&zy#{jnuJE6>+gSA)bIXGr@18Q^eJ&}VG^`MsU3?G_+`Dx%S|p|3v7 zG;Y1B(Vdht<|0+p9E$)d+J3Pi7I;ePfP6S(TsAxOZSwXK07m&YbWa8dTQk#Emum=; z7tm55tL~awUUpbYetv#d*VAL~d^zmo0vH6WlHJxUGaDgx?w31%S>M3FHe{a8ssB~; z^YfFepGOhT)Pi@B;IDIDCIfkS96!18TAAXUNtTc>{Mrl981EXx@!kKQyD>-~^i z8127#>o&*_9t00LMe`);fd?*p0}YcytFuqU?^KzQFsmz{I!y`-)5&SEKJw`2%U~wMDC*#y?WOOPvdR zuKefG9{|@&dZNlMtxZy^9OjY}|Li{(PUzk^Xwt5fhx&5y_Mdvk!*r!W(IGNB*(|d5 zJK%bTwh_uUseSI?R;b~$aHNsn{`BwalQL|0D497HLX(J8-Io2{>pSdU;LF%Qo75h> zUN43$znj{ecQdKN6;VX`X*gncKc7ZVpW-4X{df3scv8RX70?K!HwOL-pl{okMsw6> zxSb=(C**~E9k$4rpJPn&GoJnuZx>n*P_9$56;Ww1XvaAFQx0Y8g^&b_j*t+~!M*b| zmR_s=jcup?r+ffRH53G`x10U*`g?z8A_Db?px}_PgNR5FU97F;fSkT1hq8Hy=}Y|c zT>Q|dm#t5uuKA`5wN~*uJ8>FcB;m1>tSUNRzHE*n2@yveJzO8|63F$^zcp-Q$H8^%sZSK+P@^a4d6abkfEi&oDyB-E|ZQiQn=Cbe-h*of9Mt}3Q z54DjA5tl9qM#ZHTk(~KjWB^l1i0FTUsG9_7qdL#YJ54xbBK`wfv0Kam8_ZiTkEc1? zbVvdj$Q#0+-^*q`KT!YM4E(m->NOT|sw3HVKKVvcW&)GrQzpk%=gaKBBw*;n>L;C? z^fiHw+ZA8sjlYeZ&klg?H_PP*1f^QlsiFLlf`_`oBI1Kg5~v0=_iiZb=b3>aQrI+N zxA#Fwu_fp5w|BO?xyLl72_UJuO({<{DDn_nMvc(&q(0=0#{irI9|mgeaSYA!{rnsg zhwP#fg5yd!b--K%>rz)$!m$qx+5r0o$mnPn=-$24XxUuTr#YMRoUJf;9j+40B4R>8 zahen~N+Y6maC)wx5>U)-K_FrZ;|M*<4JNGt62n-51C=U43guw|Shgb~C%J9Vk zLYY*+%WhzR?79#T@ep$P4K%mjIsr6)jJbfurpE62uT@r=dm+ydFZN6a0IX4H4aGGW@W7-> zTS>gEsdmiaPZ1_cV-U@E+ACW*zr469B;JbD2z0PCKy$f*|6E8Y@DvI+n&yw@GH7nn z{Vr1W#ySVpC6m1abzMr9z4wnN(^s_iy)L8{+iAATSm;$)D=@u^uY7Fe!=Acg(iN%ZftdkcfVT=O%|(Gp0?>XDxpP&?q#=+W!`bmuaeO@SA6Bnpghs=WUgc=eG_dQ7)-PZk>yyWoe3yudhkK>^pF1K&AV!D&ad z58ESih%kqfw6t~%-`rJ6w=e&}1S>*baH%~Mw zCnsC3-9L!&61l8=0dq?z9dLL3X2sje0ip|{7auW3;*6Jhl`Eo~^kr*M;2 z$H+JKTPT}Fb5P&S(5C?PmB*D^GCONA4D5`oLBwqKQR{9eXu1KH(9W*dt?;A{oPLJD z;k7FYM^P6pI5j!Xae(BOv4BBStkmdx{kzXY1AoL%{p2mZ#oV88VucJ@1=$dhk8#U= zxyc(%Tacd>Bae{=U@@v-IWhGD*dAZSMA}%O-j{**Tcg+FY>Yt{Q|fgF95}P-FO!u@;Qm!E9cR!0cWV3V4&J%jcQD@@JQ(xKd%z~a=^|j@b0yc zM`c+cq5o1nKk1%k4-hr}fVGOc1HX9vb{Y-PdR_Vx$uP&Lh_Q)yMk_k#Wd7{o_8VTB zCjV~No1_QOnpoYV6i5^iri+mJ$+P1JOI`#mTJHNqlZweb;(w&o7^fgUOBNjf<@g&7 zP1qcyTDw8uC~Xo5befw=)HtR^#KFN^)U@7Xqai5(+7(E>iy(zN6^VU&nLI#Aw&;No zcT9QZNsRxY(sB|T_%`P#u6~mL0=04*L)+UQA#7 z9+kbG+#fUTSjxA_oP{!1rBGS@qMYYSLeUmo<9bkoB#cZHF1$ zx;v$Z8TD&if)bjqaq13BwGm=bf`oNLc}D3P%7N)XlqlKB)E~ZR3OFqAAI^DNb11zU zaV`NhU_=aPYeB_~c#sR;&J#NX1_tVy)MY1_(QG#Nj=vB!HqEcD8;p6D#aZnAq@h6#{zLu5 z?4+Hfb3%eHP}HAaWlj9=&P;UbZBuZujKjO(XV}O|NYdS`OWZ@q&BTG3Z#$5i_t4G4 zG(!b6UZ@Mu2bE2)!ihQdKA2*qAHBKT|LTD;R zNcF+dH94(zXfnc^`fLs2x?$=o!X*yWx7;k){rzHQW(KrP-?#eWVu`a;t}r< z;dAl~MqTF$&BTXAplSamBrn4_ccvDfv&w#4_?vW-l)Q^Is(ieY&A*v~!{knFw7}>zo<%T$3EF0xvkJ(>FHB;iQBvRQ zcp+t4;=k89MwJF7B&762M$Ir(yYW+_ImQN{iRlO8XsqAs?`K%9{vZfyy2pbU`*g!X zoadYLfMilNga?O+WGP`L>HAxqqKb-9RNjpQke8oftEU4SGU|8@kK#y(B7Ucvxp{8{ zB@@IE4jlSi_&zX;y&~~e|BQ0?C{EB+RIy3zoZP+)cc8JtL>~bX193FVgC=aySBCQZ z%tI*U`4RC)fnhJct0E;dIGFNWq!)hI?YNt+$F+&WL4T|Tw9eailY#yv0-i{TiHV05K@tVM;>JbZ87A z$SyT>F&;F=D|T>pWV@J>J*RzMnIj0L)BTvo5@SuN&h4g@`pgsvsYS}hu!kn>IubNi z!wX#!T9I4{X{J2(Pb9Q+k{=p@J$&em#AotBoWWm;ac2G!^p$o8<-E%(0*-@&;N}jS z7T%5UO5g`-W6|QYZL&qeH+WhguEvkwb#AQIGTV4bt031H zP!>8eDht0SiXfC8&g6+&ja~&LzOiAzKKr-h+?S@pxf14K%Z@llRH2S4xKu1etu!+c z_I_Ho8YtSL(%QxpaZ0p^aC$A+1en;7J#uA(Eif90)T(-drPgD?)H4&gQsL?+e@Vj? zbip7MR66NFbV&g&%?Nl7_TH^3r_+`>;@%$e#a_=UbwJ?~m!hb~G)2S!J@z?uwSXib zop6_BNSNIrdW1Q&zd(kqSaGrb-3s-v9BljO)plV1&NuF0cOuRT#_tvd!xQz7c`SJ9 zO9av(<#Io#%zvr$%Wf-#m}fdy>BDz~8t5L*K!;C%msTj$uh;7Q*sl?A@RdX@SR@On zg0QnElM|-0HY_*0S@qxeg7_;TbLsj;4dGRgb(jZ60-4GC;ad*l>!Z z8C6Y0Ck&Lg!V-wfI*l_Mb;HNJ?7u;(6!gm;U*8Kkk%sEj7-lM zNl;fbM-*~keP%*qS#!e^ATm4ZcTde!j;Q`H?Ja}8YGRNpAMEuWm3qTrzf3lQphkv| z5`S(kZ}vLkS+Z>nri=o>o&}aWXr&u;pyik}<0!$AZZ-2o5l&zFrh`{QcW72F>jyl? zE?ayj6Il<_K@#?f#@%gwg$NKgj5FC2QO#Pz%{H)T zq0^h9=DBo@a|3!&#}($`+P zO~7xAC8p(ET9it>sG2&49We-VuR~D=oy-2?tLt2z%qyRgl2V*sMC6Np=O+~<@^7<=hJi9>rI`c~M2kT{F|o@xF2s|M-ET&hY_`wHXx-GDxD*!C@BfgEd&_x1**B5tC{|?S$2)8h!>1@VL)-IO|qPa%8olj zS0=s00{pDQAs&S=SHw`7IG@$7)$f;0r2q2;0jf87n6f%Cyz@zw9KayZ~% zxmrk&#-KS)`#CYC%@XB1#F;320h{@ZjY@ACd&1;;&ywWB9J5kHs#FL3=P7SYV_A|- zsU~7X*u``2P4UyCzC?ud{|yQ9mFw zr?qzC0A})Yc89lwj`k>BC)fl_$4L_o0K=PIqg5j-`tj*iT$V5yk)ja*6lz`(5jMb> zT|(P<_WWP2R1Gj>f?fy+-&zWbT=xk8V4h^a(MXyo0!(qMtFO=4K3NH=Dr;@kdw6`* zSmlw#JSTqC1*C8empcF1jW(d)G~!B8o1fp475|&Kk2+*J#*>izEG-&hYVN~Cnx}7S~kc~L{vR@{|(IZgB^aj!0+Z$2=3`qX= zwXl|?dUU-feEVvD(iJduC>R)IXW&%#@7~*NlaQXl01wVikSfsk~Lus@!Bd06Ve3_nl6wws-*~R z2&!3U6$6hP>lrpqn8#ctCoKc@t3Lt0uuX;MnL>*&{Xcn(f6#y%Ne1xm=yZi^U(g>0 zMaFM4q3DMoZE|T~f~~@6mN1{0u_{JtkB6?hx~cR9pd`s^(y9VITGB_ptn_qlFiJFj zDpj1z(Hzg~Z68-0weVI5m@NB)a1v1nNIwA$l-jt-r6qg)<~lMoT6Q#aGue$en*Mr; z9HHXxLPw}{z5k)mDz)Hms#)O?_8J!+n8&Ipw@|or_Hmw-_gR=l8FP6t?6mqe4|swE zMjGMwk>J1F9O=}a069gCfrVpPN1(^DOV;9DM;j z08Jg;WM~2*LMRVq6?VQqomsKwRsR*6RtecV#&TEzYI7{-w|%sFhWrq^WP^iK(Tbv^ zD9HkUH6*-^pgAL8GOhw-O;=g)Db;|uLQ~P-|2gLF=d5Y3mt7(}BxMH4_7=qsHxIo# z^V0}T4QRl9pmq_K2bEC(8aQPHEHQjvhc(M#iTQDZGMbnOrOBpytWE9PO_kD|GD3rdri3PldyVmdBl5(6AcdP$!Nb;9f8Q9O*$Ajm$;XF#aXgS zxO(oL zE9pvcAix(NX&#xuf=#!)&o25hbs)OmWUM$N%riL5K!W)P==W{Ens|Xh+T_Y*T+1ZZ z>ETT@P+T4f_8)hxj>|3I23xw@E-o%e2c#ykddZG&=A%ew_74yFxF8hNyOI7h`9wU5 z47UG=CU8(GezXTmn^QS;Vq|WmKkdo+p$Py*j!<5_&5>k0B_nD4v?uyOypg=Y`}vG zAu!ZD+xb{3ZN(US2IDDRPMz`7P)JpCWNfXpddKU&nxt=*m6{me#j(PaVgK)ljI2)l-baeyZbS5#{O zc(HfL&9>Vi)m=y69Px`oq~fYX4kk8^Joj4Kv_q-ZW!8$*lUVV`jA%;YE%4{!DNU{6 zqHFT3hET`jx;yslB5E_XI14=@P7Q#6JGi0N3;SicwiRk__B5fRh|gwkV34-8H&2fj z8!vC*U&ychd9b;{j4ikw4;!hVutLUA>@JL7Fy&A44>#Alqx7qIcR3&V?4zX`gJac7 zq^Ou_fMrgl(PvQpxf6H-F_7DbE)9&sr)6yr%Jve));GOV&Xnd?iI=}MF1BCRWi|NV z5;-7S_?sy&)foI;_d6q#e~V$|()=8kSfKvF0o}#u&}5CL0R)>`n4Z$$dSYQJs~;O{ zU#oJ)%f+S^Rv@?J6A5yN0e2`L&T>{ zCc>11_EX1QOqn71x9=DA6N|VB7?jJr!u(9{J?rJ^L{pF6zvFrnn3DCLhbuonpxNKVk5fpkMH3sd#-WUzAjgNpgZN~O$zbqRG8c#Iubm7?59D18=IJ!WZVq( zPt?gn+|Z?pQNc5oTFN+Pn)%aiy}@$E~TChe#0zTT+_QST?5La(bf)*e5!)tFT^+>#U= z$9qy%46PE?fh~z$`U~5EPH$;~#a<;BkH-b-BnK?P5*p%SX@U~&Je(<$VpbUuKb6ir z&g;^G!;PO$u*&C#r@Q!qio$J()w&w3l^4@DNq5e z02ZUW>zu@}8CSG0KfGuad6op(yZ?gVpoGs=52Y4w+EIa($v5a#YMGf-48Rb>7JZRGxsJ#G8ZE5vpnj)`1KSJ!|0EUY zT{5!J2RA=G*SDQrxb0ld^HmOdQQs5aZ+#SJynctp7PrUzPMHY1O!_L9jNB#31pwic zu@p_I`|XUZ{)v1M+-hm?)%td@eI0i?gkRjrRGy+&DE~peW6Sq_uH%rP`~2Y6*IvA2 z-ip*5|4sZ3Xo=rha{$PBZvZ&dFuyOp~e9o0hx2fpNV@;d$i=L&wk!_Bc zP(ZAO{fx5QIFYgpTg(tH&r{-6^mR8Sc}~K0DS1I@iOP3dQ@qpXxT95l##oP(mgS#H zh!1_#r6~u$T{Hee@lp;k63{oG-?qR-+p9IwF-UZ8Eb^`K8?B!Ht3HB5P0OBAGFQN# zTA5lND_BzK73=fy<5Y{Uq(Mdx{_+H*p&t?$G1>0@LcP7TW&$uwKE>U191N;mwBIm3 z>WKXYPl*?;<0lIxbCaG@>W){u^! z^Ne&I#9o+;B!cRXPaS`q35zf!5Jzg?qq+|Tdf=x_|15udypcKt;6u->M`s) zl@wFBlrp*sowD<*&b6=CO$p4yk{@j5c9*6Sf7bGF@*L9k@VA`kYl_m> zXMgmn`1dVp&wRn-X^0`GNjLgzM>e={M-Z5wthXb|?9qw`_3p*JD93u-o_Uv}>E-AW zWp=I`6LuxITymr=w>ISy**3wLv*cIXD|Uf~#rm-QUp0d3OT8j5w-7__&rc^!&$`XG zQsOKfbMbO((rMCE{!)IQWeW0&q#r!8Z-_jS3;cf~sO)Y4m0HAD(+89C4?YK_u9j;F zy3T)VTFq9+uH?giS(=~NbuZ%)n&+%E@cXZqhBDL9^S9!_ zncBYQu#401vbx4Z-fH}CRf~DGXNj(lc-`gfR!)3?nmR(!7#){^nblU#>V00+ism1% zM{Ayvm_5wK%YQbETEot(+`g<;mTZcy|A`L)Wt%mY-LUb-P{*KMih}gIWLnfQDZ)=? zZ-Pz5gg--j#v`F9v7LuWidU!N9edr->3hy(lB&<_ntEO{L#f9e8_}iivc(MQVJFU9 zk-z#wAGzd~#F-2jCHD`h5*sGwD420`sE*P>H844nL-gv-TZaeveT~v%)mi+Dt&Gm( z-$}CbE8eATuID-1*(Yh+4Ib2!>4uPg=8rMk;T+n%EGSGO9xdk3p+QqRE~_aRl-$k# zFW0$^@RoZb3Be+Cqg?URpQ44*zoc!y^Bz`W#S3>qKcupzw#jUg7H0!^=hwBD%o||A zP>G&)bVY*TNdG!*`)}c&{N#Z!hNph}|HIf>26Yv-TVDm~2I=lDX{5Ua>6DW0MndFI zcY}0yr=)a)bc=L@bi;e|oKI)wyfe=?W*BA%JMMk0wSH?IjwZ3A#9@+~k3{D2Y1HL@ zE7C>CQn#fSrUi12K;f*INz3s^2$LWl`C7jaRUxEU?3M--<1#{)Jb2<{Lj_~<79Lq& zNllt@KQ6{fyLTsQ@ZUV|p2y{Pj+N!zi*Io#n&s{5U;n}l!cuP-J64^;7l%&|H~;X) zPqms2{Z~c!(cuH9y>J8PGWpK*47{?(QP=h6Wu#Piv}nA3>%Vafl*D1q@@v`iK4eFS zo8&%*Ib6hxPU8+ccEJ<%PDCmv!Zdlc%+O;EPVC#9<;L69l+9FMulumG!;}u?VH;6$ zzmXO8L}&(8`ASZ<-;K-yhF5+N(jxu(&@Z*kJ~8v3udg(Sjn7^8=6HS3fel0A8@c zVY(Za@-?WiX;Z4Odj6&z`Ze4X7bPCwYWm6qQ!U9xSYr3?iX*@GXC}%cL zi8(?q`qmM>03+^fckj8-VbIvodkIxq&!VM=(sGfr{m*Gtvz5|NP{UIhf^7&#LiwEL zu3R6VJmr@_=8bJH@$pN4`oj~JMd&ngm;Af51_)XqN4iIMWDgXc>M0=+ahyt7Nvgbp zq^aymp=nIk>_ExI+EwW=P5^AvMitWe6bCsu}7#58S2&B-Bsi<|+GtKz_kdyTa2lOGI=w4fRXq9)EjXkEv zgbjU+SgzXkuc_Q>lN@p7_>-tjVymKQX{YBY%T*Mxpi(U}@(Rk_qv*tYZh(!q68nqs z0w0BBFx3xgEbXznc)Qt}0&!dYM~6>bZ{rd!OMk2~#j(}vvItO|dW>-wm}*_`zP&V3 z8?tnnA}_^CaBzmUFL`#E(#L=oo`1g{XwDC)Pz8AR!ZIAJ3sld}o4%#|?!uudKr*ez z_K(!`zroh4JXaA~DdkaG~RwMO{arK0ptN?F|<)hD6i7~!AGHFp#R+^LmBH}5t=IsFNX zzBckrk~UV{iWui>|Mijh+M9Btn`rIuwI*~rp@dtHL>f;h#knx>Q*CbCr?atP78$2| zOT68q#EBRYyrYxYm^G358Vn5P%2h+ z@yL$g^d6YKPUC5ffSK;u`?llx`g!Q5(@wasHfaSYK~HKqY1a|FELPIH`uGX5$&ZG8 z!LmrG%d)`1BPpF<;XD)Wf}Z=(Q(?fFU}Q&9a1)4;Lz%Mm10{IN$EDI}sk+$EqI5oI zT{mu97pkWC*i%cVO$vs(*+f)~=k+6wik+Hik-l+;sl{?mt*=r*#)(wJ@|fUhF`<^}IjTPPHR?8N#c6%g%-%b>z?HYTF6Yh?V7J zYq|b;Br{7FH&Pdv*qb3|Zn5)D@)Y*RU@mp;p_wfgRXIiyMe5@SWW9+U*kdWdvs_}Z zj3`%rTXq|CNw>NaY+R%^qp8=J-JGUZf+h$@tXs&XZ%)&s?^V%5T;>K;RXhTMxR&E) zIiE8cUzMSuf{jwp5SmbHHa?`$e)i7e_~*`MqK<7gD@IM%fO;9T)O4s88dL9zE>t4H z@(w3I3Ss-qCV3xy$d46NKM%4nPvrV= zWcNY7H?V;3Kny|8NxIATOaml{!5yJzk6X|k%Iph#t)#&830?qk%=e|_U3x{Hn8xVoA}qA9s9&- z_TWKEicqdG_~_$&8EV-3Ae3Gpr2og+N$$Sh15XhN++vl7^VQvOE18cAnS%j{I3vwf z%w+MJI62qgK3#261D+Q!PX!)LX)CKz^iNHCWbtSUTx+Bt1cW6?i~d7B-u9E+G0|hi ztX>gaqaNkGsJ!Wd@Mi)JMXdsnLnmL_$;YxOd8o(X^5-<^2n6F_1+qGvX_fht9Km#g z7r(_M31ehD0UpBRUnlI=i~B8{Rdl-jVT+}3Hi-7=&&gv3s9xnC+sp5RoQKn9q@lhp zFDYT4H3i5BFfnHnar*VwDfY|fUIl#>D&7M)ty##3aPE8EDsx2DYMp?88sgeQq$)r4 zO%Ciz>u&@L>4H+{i{c*Q0_x$c{$0))Bt1WNJU3Qf_yE7;f=P_eo}57}O@@0{Z~@NL z*Y-Z5ax~E!(t=>RE=%BLT>fyo8TUoto3)yD7;vyau#FKir%cG3RxNaRd`4u#{VsV7 z8g0HhQf9u1Rhpzs#VHOJ z>}gUoL-=nA_&Rh7^=bAB_HyatuPIZ%6wxZW@E^d?QxX_@y%dbfpJHa-K+r`fS4h9W z4Wp`4&dR_x2i_mMby(?^WIk-tjJnAWsP`IBf+FS9pRKLBKnByyW-X8iV4AG3oLq;~ za2A$0Ba~J9EWDI&RU14luFRqsoPNU*;|tdfTs&&-?M$$1%_gTNmm4F<@i93{*XA?ITQL?Rps z|7cRbxhqeEM)5K0)afB{%5tsgYdQxrM)faFx{YFjcU_8bwBM`shS z^Lu$%s~nKVz3Ug|b~=920HG<(&x717gMqhS;*m5%)%NCI8x~#@;`#r*Hc@VMV{h-a6xf)S zjYxj^)slv#rn(N{w;0UYnpwb?6#fwxCw)^himFrx&5JjLXRS$j*WBWAs1^iiJrYLRP-hV=wMVl?$EOr6tdZ}yc z#)raSsL23@lo(mFkT^lQ3SqB=Oni-DE5on_6~I8+W2c4qjh!zhgvL;?(hGpoC0g+T zcOS*nu{~<*$8{H+6A{JiC*Y#r9Aal1zkNV;)zbfX6&Vo$ysxV(MOmcyHyd9*f7TDCD3by^ zCkFy@v`*mu6}MvAwgQs^mV6?dr9P}9vnJJ34-jZ*QX`A6qF&&CnRpX79QGQ-H&$Vj z1?wx1n4QgTE6_yFhD#yC%u)yflhDzN=Zz7`B;BZHbn|0-Vtjnwa^bNfSLX0?t1C<) zrc^MgZG4+Y6)`#*Ofry~DcwHyErJwwFPdqE-myM+=HNYTu?C^vJ88{m>pqZX4GUs2 zVA4m=V{i%hi!F6e^qE+%2%I(dkp#_8v|BbJa1{1o*=`vDv%rAI82(ed@;6fScXUZP zKEDfQkUnv|#j;G%w(l~}t_NwjFu7`6oVN_&`sPd^nKcN5qACw|_kW|M!@ftcvF`!c z8+Me@z#Of#p*&3>mb56`&kyV_1uEuP@vNS`sH-;5MK99j&kv-K<9204HA+ND+!by( zqM)z*sBYIs|2eX}F> zru$yYj}gm(kv41sMMYOz+tL=B;1Uze+KnPmg2|vU0^t~Ckgk(#>vFv)D<8xD#N;_p z075Vzs3~Is5f)wq579w9C2XA5ACW4F!Y*LK8>);pr>{#BR2DLlG#tJKW~}!}iC$DE zY9>ZTT}?`ci&L`Ty4mUv&5aP!K2sw+SHWH3#8`$-H37NB&7p7XxTb7Fn_h0k+x0)@ z4?p#Q%|LXuqA%d0t1SnmVn+Bub5z&>#OR6+SR9ecvftzeu=O!OTT%^#lD}Nk->QxH z-wbFVkiT^14GjJA{CB2BcrJ_E*0zkLf+G&@W4NZ_O)s0~m^rPNx;=H`k(YZ%6antP zfPN?U=UEA-+T<<+I~DY?=%rc(!~nfw*h)AU9GwhS&v+YitYE#{I3v#*yLSxNLRZ zfdFHr4&dD?{W>7{5ucEdKihBT4%FXG-~nRq=(J7R<9@knfSCQB6b=mm4z~wzzPsSw zLPMorYnLLVm)b2iO$09h3sbnJC?T1qYb%G9R;_4BVX($h4lA|*4qL0PL4PX4TA zWNB_5{i4OA*CsOd;Pzi=NAW1ZF-8WMg0N!TOFgXSx2i8>vFe0m&B;+t#KgpjAW&2c zJm@<+k8R)|_maGSGmBHuq)naMO}-(v4?9iAw*{APz>;Zz_*m8asG60Rqb_u?N`)mu zV8;*ap4N+XgPLwt`=2K~{OpMYJg4E{8JRSlG*REcb}ly9q&mcCuXP!~F+q@zRTw0b z|5DelGNGBl;Vu`>kH1j71jWP+XYvnriA00Eg&}0kv!)2wbPyDykS#PT>#uv0R1|%n zmH7R;*AHKUX&^Wqpez%c0Kr(BG>{M6<`;S@Y==RXML*T)RZ8A^kmi+URcSbc_fL25 z7JvCzDLu#gPExNhM?23jfg&=DF}msrh4_Ox1W_H>dEy9#e3s#mp?`yi`00y}lpL=5 zJ1G&bV-I7HOHfA@?MhC-I2uv##0zjsw<6i?sD7@XPqS7XUy#BzWJ8kyV?E5BtV|nG zuzRSszmpG0_mUTf?VlZhdYtUSO5z|7M?D5LOAbwVtWKb@0?Z7zdW2d5Xf;?Laoc{0 z5`HSJx^I1&dDBs+o2M+nj3v{o_#UgzzFd@2s=kUuos8%Zz1Tw4&sxI)X~5`p=R8QJ zpw~meq6`2zS-~;9zK4-3dmPS<%*xROZ6D6Q%Ls=q3;nrW<8 zpbRP2_>kSJaggJ9;}-_xW0z;W49BakAQ!SLX|(TX-T9$|#02UUF889*<(D8>69ykJ zfvocUm4Et-pFu^RYBKLTF4E|9mwawh=@n$BP3Bkux1H*2118)v2k?kgV9Bc7&S@W1a>4xGeTV# z@RRk2lIy0U8C~-(Z1Lb6!XB)hytoh86g44elCK;foR+CsP2+Q1bbj_K;C248YdNA6 zM1`sz(^QLDr_HgB{obHCL3^YPWj!*1GqUN*H6)EEYrRRiV^03W>9w#lX*e2rFf=qY zHenB94s5pc+$?H^bfd6{HQHB*z2|%3mHuir(H12_%u_KdPSI%6jAI`1C+$Wi#xdvh zuv_T{hcM=E^Bxi?T_MJvBKdzuxmU5k(!iQF{1x->Fq&zXl0>B`OPAC@v&s#*>)!l8 zTf@3c<$<|Ip2Urx#^hFY-&o4Et9V3BKJP6^9?d$|X2t}AwVo)N;Y{`(3?rwNsVLe` z4Q@432eF6>q!X(Ss!R1brE*Y#r*b3zD3708umSbpSd!)ZAWh zA55wm7F32@>@>>edXKjvyCtCvcZrWSgkS@$6nfI!v_wWq!TGUGis8zs+66WM*4JVW66{*!1!4^1V1B z_R3xomw_ZrBAKh1v9~@ue2LrF`vt`1c4^!paV|D7IGWfnTmwo7BUdj|cy&?e z`rDX&KAQXKw~RfVYh)hV1MnVeUWG_~>+A*Jac6FltFgW5nRgu|%QMePEQ>V|E#`i> zE@*2|5vWo6qd_30?_U*}+Qjv-DV18)O9^r7tGBWd+0K=?J$V^5;4Zm0W`+$fGzR2= zup>gK2JRYWKt2!KanEbLUk#r3^A5L*Q!LW`sv|uJm}o@K*#!r4iAge=b7#G|7@29Y zr-=T|Pw2d!O_E%;OVo`vi==a0sxn#wE*@l&RnmsMu9;IE^@@Gqlt#aka6sI!&!qS$ zn6#DshKJ_^`Ks5^w=@kmi;sF4?Zxm5xwC%*uVm>;|H-*Xh%eIr$Sb2XV(h1zne_|t z+1o#e)A%DuBc;@{C2IDw)wf^0I2_unk0_=%bi=4b->VelWKJljjT1i7Ovy({Ge$z* zUvAzwF*IP_v0!ITvnqW&xTzK397vH)4!37IfNY`D>KhW`W2F$f;s4bE&4F{Un*`(S zwEg$6m#;76#7M~+b=zdPY)WE>WH~g#YGN$P#xDO`NZt(bAloW93UnZkRFL&T_Tm!Bn+iRnZPq=#9|(oc)YJRUAIOynLf zYudkLB2=`DOvGBZiYZ>TQxhCi#O%j@3C+bSWhF;{yV9DFFm4{`x4_bV+3>;RrU*hm zWNIu?yzK6hdt>}~R^H*G+uRJ3SD!B;c(Jx*dbE#x_b*{UQ`9)#r#zw;=xw4=t+(53 zx!#Q{UjELP&|BdMhk?yMQlJb(aal4Pe}vosYYr9y{H3@;Z{JqWFDrKT37XXX%D58g z4W#N2njFWRXHE)fs!n>4m&!%Z)08ZP*_w#+Wv03N!+HykHqyrAHW*P#9Ia_2`)AKb zWT-2lMrS8v1gr0KHyB~ZZOi_;76U=tL}3PHVa_os^z+9kCuJ08fo#zh?b(^b^lClT z-V36Y?b9iPq>rrRE%p^0{CUVb zt};6)rJ~Z}pPzI(Sdb$k&iZ{XOm9;%?;+FE346_P_*NH#gU0D+5tSX}EftG2zGpPg zQpAr>3}>a8IYTXXn2sgy!3FAS8S9^sKWO3 zYMI+qVN`8WrJYPI^V=0Ym!0l?glaeK~5|a+Z*)^G*8xavx1n z^VhBX4VszgvgrCzP8EHexVen4pZ@sV7oFZ_@;YfplJHZnJ|16hU8FcQ&j7?8y?t{F z4T-M;3{22w0fB9-T6yW_X@5pyf2OBoS34W;;(}>|k|7^u6wgYp=}b8anMuAs?q7oR z)wsd=V56p~rE5Apl(GOuCfMsxy zjl62hQeQV10xY=uni9)WFQc6uwVs155)@yNj1T9WyamI#r|U=Jxjamg98=#q-t*{D ze7A3NxhoTFa6@Ua8eSY6?zz&!ib0%3KDWocQ)wyEr)qFLJ(vkXB8F?^Ejoz!z$W8E&juS2%hXV z*kHpGctRKwEUDU^Pjws}cIU&l99(qT7cRX$bR-UEIZ5KW1;%Nl_9oVa%WFZwhRf~7 zi<%hITr=ZXbHZ8ARhq9Cz;1Y`w3CT5;{Wew7`kRel?qvFuo54Sm2eZPvp~u5Lh^F! zHicz`ZLFK8el)?N5M#R7$v{Xa_e}W`%}Wn)<#2;0EfARfQBwN?#@wcI6-4`J3gx>` zZH|~MIs5W*IB4e#Y85iR9`e$qx6%rEJX`WQTk6i9v`I8r>snpZw=^2;18hovg+k5O zd+mi;u7Vb(xTj*eg$C2~b|iRSpR4b9Emd_n)^~)YBW#hvO48Y{94ss5O?2biIVez( z6t3{ycD9=zYZ@=T&lFF>=#(&YiwX8CVp?W>wIlZ*5cgxt87$(yazFa2FRNzo}`tjH-HUkXv6^nGg>;c%*OH~0~_NE z{pZtPMbrBD3IAfk&VHua#^Jy)%ha)x9oQBnw9OsIK*5d;{6tJ=|G>i-6I)f+z&W(z zvt_o`wYL%5G|zIB?^5&Sk{+)jIeN4-H6O?SBrz%fRBtLlRyzALUqf(Mf6aOEfc(HY z&QoXb##e41$p`pMt$wpNfn&`Blq(bVrEzc2LFN%5)t!cBV*jT5q1P`E!7|cmFM^#B`Sowel1sAJhi*}ur z%zO+5@vY5o&FnPy;)HaV6tyL)L=^oXS8Kbl;GPQY^*-!4_H#h9=!u45n}q0-3LUaq zaTf2vFBb&42{uJB zaAQI|FS;ZaX8zEhq#eZTS-A}h%juF&K%o1mDSzw3_P5z?zt_^wu2L%LjPUmE5@PxmKUD;w@7&1 zT9e4Xn-bc-wfiVp^7!b%CD&j(bCq7@dJb*od)b0KFE8nb70lg7ZL^Xr#mJHn0P2FH z``{&W>X?W8?h8kSfm%T1IZ-vJ``Ank^6lrC?yws+%V*cMYJd!1<|95^uYVtXXBEMy zy=PghY1cEfBWGZ3JPz7T=$mq>bFsGGZG!@gO@AGqK8x>u5W>gC{58oqUMrW0qz8jl z%^dN_EJR=|WwLblS%zgjhHp+|jJ9J~vyU!$BH^+QyVLmYy_U&Mg}uSmd8AW1-fGD4$SFsy@`y z(*zC01|L}1gA*oWk=bkpg}kmaQ;>tGQ9N$?ZvT2xb|=|;AA66FWiqdJxU{x`kjxx6 zk2^;CdEO|wI6Ir{wm9>SPM{I~7RZlAeN1)pexU|Fx2AibYuT7NF(Dzv&qPQB2i=lI%I!5Sg9g)JGyUX zar80d8?w@72O}66FO5_aP0(1e9%`0+Q8m_6p9@F-D$&fcJEgzPmUBG8 zpoolbUA=nJdamek-LlFAVjcw{!Cwg_y`D=u?ig+-liE#sFyCmFC%h`-XJQxJWsE@KDBn?P6m}`tK~Jyl{cxlDD{}yJ5It$-b&|Ns!R7~_sHkR zR5h!?ZYE^26(8frUtOWQRH_e_gJqVAK@yT;Prhzr7-VtgjdcpP#Pk_Vvjp*7=l~N5 zmc=ToKx~kZMZM8{=^w?y_vapcIFOs(Y5IUvEMGG7LZ!@?1^^2ytM^7IR;-41S{LKE znLum@;^SEWifWk!r|n{VX}Co$bI8!lO>@0Ef0 zA5W3ZlGHT6P>1D7gdG+L+W8o_Al?RW3bs%w!g5ysfIw*B#0V@<~Fxd~k>8~J?mu$1 z2^eEYek98?8%txmzl$NcMZcZz7yJ_I{<5#xl}N`Uv-R{QG-`w_vQ60vT?y}0K4~30 z7w@Oy4WksjT|lekIwv!9aPhnAm+Il>Wn;2L>yCpZsxDO?{`cuw%cL76hUK|o;6N#a z&`W=FEJ}fUPPk?0rfKs{SG5lFkk83JF!`MGQ*FPO*r+CW-5unX=DewD3(U5|3}7@tGO$B?}( z>Y3JAe8+(&dFmu1bjYM;sKYp#N)?)+i}%H9NvK8HY38%&CdG5+K)%ydV*XoE`~iJP zG~@YSf7GGkPle&QqMOkco-=A1GRDRrhDR0(pmMBBpcx?gh3t20U8`YZ#@;I$DXOiEkp0HU=(u_>{!y=D%wGL94TH-8CwZz{nDsKn zSwJTC-idOlL*YP%j`%cvLe}t6sC%B3^?$pL@UFvw$ zxn0NUpv`v>Uhn^BPhOPyNf-!XqZ*M7xmVQY&Gcn#FF;kJJl zt+jScBCd4N@JKL-{X7C*P=7Kamm?_(-6} zs57hBs1vRj8jch?T`x~_Z1O54{~17}tbgc+7pGpm2cp1BXUEe~$77@8@iH$azc|Jy z%i|===sZewf0XaT1~m$N8Jyt#VQnAL*KvXCWbmo!AkHsY_aa>y`FHKUoyERml?bHs zEW}fhtL#{^fK(o<#SL06hsh*}lVYp~Dt3NtCL}|WB&=@T$Cg5S?tbPVf=>=tZPK^t z)@owAXW23PzEzuMVx(zyGd@b zwk^{Tz7~HU7He{Cw~|gCaWn=;+%PDn1&}g+St!@MvqnpO9t77=a5F(d4~7dZZk6co zImX-+=gjWJA8#|CJvJmIxb6P9hU>@p%gr)n3~1-00U0vD!8TlYp9(>pzw67SY8+G; zwe?hJUehm(@ZNM>(`%1W)*&|Ruphefc#v;jV|+Vd1x1pbNs^Rsq_A{O`o?HqNZyp=5< z1tZ^W0$!V<$uzpVKVygx$ndtdLe=$Y$XChmgy6PX*L4F+dIJ(75M6vA zdCPvX#Aej>t%Y?>StlFhLkG?bCD2V-ht|Csu|@O|a<6kXBsCpCs_wlnP+A#@(8M8C z)+q!b;__OD)COQhVKTRp*Yk!#qb2KK1e@+YsM3}1hGvj=!K(Gage z>gU5RQ(UPEu5k*ee$a0y$u7PG8U3JxG5+!OB++D8{ls+WTF`7eyi$_FLNd{MboVx8 zIJ{sPSRI$IdF^<2fR!qFN14yMyJGAbplJ5+=M{ppnH(X=0ebaUv$xHvq{RY#<_^#| z1VZsdQvQ>M56M?9F9Bs4qyu+Nnr_da3H=(LyQ}qWR%99C5(eo40 z(S{s?{GMZ3Kw!}o6v^4pS0vA2+?t9G40g@k8KcXWFm=x+A$g0uEQ53@%E`qMREN>=MtO zT-K4{Pr5ogaYoYEn|t>~cLR#zfC&p$YgBvzXI%2DjKRTQ_|SDCbrN+lRy_Z>rF-nF zBI4R5oeBEa|6N{Q7Ua&RbN#`Q(Kd9(N5VzKjR+49m);GHjzeA~gGPTdJ3Vdk6_QdlSK>e!|^&Z9{lsNI*0AitiikfC!`~Hk>@gxeIqC&Q<*ZeKJXe zUm^-Wup498oQI^_>Xf*_Ar@(#jL_A~rY5$IIG5;sAc>!VMJyww5r;K_8%iE^UZwv! zf!PiVo{Xg-q{X)M0Zs>?BIJ@zpR^oY~zb6K`1qfvg8I_g2mk3Sw2Fp!L_wU$IPoRu1y^bScd*Yj2p za)tiJofR0-6)(`MjY_FO>rnCAWMQEslT*ORCvFS35tc17CXD7!u9tGdPzu$4q~2tS zFROcveoC2Fxq(s;T*vCZ5PQ?6|FMuj??DkQx8JH(pLV*SRbVhtiErd z>p?-8N!+M{^a|xJhQ4gLSGaBL)OO9&TpGa!U*~VN0%F(3;%7hCTJxPXw|+Gb&=oi` zF#Wu3Pew@x2a}&&uWhMKFVT}Yu`$b3iu!>X>V06*XPCFuu>lkn(b>EkBlW`4zWD|S z6rx3!B&g6ow(M7If10n4vitld9K1mJol)N}KU!aVrl(cC(MX@{Lq?fm z|FcZV{O!1@_EUQa^4k6D4JKyV*7#WIJAp@z_%ZnkGk+*D>WA2VbF>@$tT@^vs*CWQCd-xqLhS zU2<#KTpU>%1VYKu$@TnAj&hmJnaxX98h1>Q5Ho4B05#F_5<5(dFeXm^`b)@MI$3r~ zq`9y(`y^NLV#a1hUs!a;_byya)k6={kK4eJZl59ZTbR&LS(KOp8BT`S+KPzn^LL~n zdZba);%5|fn>cN(&E!A&zg5d>Off|b4__tWhdVB4HAwDzbN(Y3yhDoUpDJ?(4C=neL6zTHZ{l@0cLC=w^8=p-Yp!|+6dGiQW=8o6gQ&B4C zEYU9#FJcfk9tkwJt@E0F4Rf*K={yHCuGp9u!>gIaMS9{Wnn~9-eLItmiN8+&o&p15 z2FJ#Z9fu^mP_AsQWu)zXSL)y}OX~j10>$c)!~*p1zl;Ryrv3c+lP#hTd=KSSS67cZ zH8qiWVp7XTx3;zptg1toXVWn=bK!73_3bF7@l=aVbPJ*o@tN34MH0$5P{R9NH3Iss zGy&}G?Jc|4jXm(v#DlWB;Z~1x`LA`rnJNQpnOIm@$pHJPp|8(OMMcH#v_0bb9U8r- z!R0_bh0`k0!v`S5*sbSTr`oF(GCnLE?)>fa-%ft@M9g6xjYRxmzSVcB!Dd_Y-xDxt zxvswLk4;FRf<+-@eIo!!Z2G8qbGzX3Ld2B(3m^mAJ`xfEKS{$vLq{ZdKL2`E+lwLD zYIb$FP!M#mocT^3jsoco5^A^#pl`9gVU@liAYeYL_=fKHT8!mqRaJE0H%jP5yhYB% zl6SmIpmYGxR@YZv*V(iZ6D%_PFFMKce8RA{%(ys;sX`_0H}IWlTsA)z%oG$9lKQ`}__&AhR9sv*FErX^hLZ^W zIrM)cv;NlR?NPs{r+Fs85o3xkL8OA_Ucn_sq-8ydP)@)Y>!Hr?sFDt zJE483)2K6a#$a+E3&lScOFpWe_ug>&J$A}-<$v(@-lf5#!d}cX)#r$9-AM8T3y0wY zdoW&?aMN#xwMe}81$w)1IJC^J7tuC8J)sn>5^zkvjCVtFr6T)JKXetnShF%`&}S)n zt=3|48l$esRqKuWQ8p~d!#9z9(J>I~2g?`YvGZhn!goK`5<}|THW_#kz9qg6!eLHP_UA%LIm$R1&utu-F$HVcvRm_3(p$^ z9x^=vfktnF6WeGV{JMDNMc?XtfU2C(k9Q{#6$c~s-xPsv7GlctB;kl%)$T%`ZD>l-g{YZI-JOrFzZe%N{D=AmMQgz zq`wb^j92+(7vFMQPSbwc3kk!*A!}t@mO>>E&|wH1%b-8b-Ny=i+Xp;S`t0HG%%4B^ zB94>!LT!BeHwh*cAy_ALDLpq?!A>!+Pf{P;0gi{tx zZ6p#-Wf9*&joRQeKN#SZ3_7t9LFTjEHJ9|i&VdTj70e-#OJoc|8mE<9 z63sTZqWxBFjKLP)rJmMj>E=F`LSD6QB_L)dh30Vvrh|=xD;pjRBf;~3Ink_j5#5ox zg@GHB_!Qj5Hg_(k$k1pw_~EaR{LAOXqVU#euqJ!x9jpIPaY5XxP?rws1K#XXZFDu9 zNR8u=QA3RQyXic~qT`5?6{N}My?8^La$t{^-moE()1b-Msp)9KycUB+Bc_JX-Fa5^ z8o85v_)uuSc8NB!J08K@(D}7&x2sm7aEkZz6<;Qg&%KW*xJ?Cgp@_*u6W}}+^_e|K zn{Lo8Bd9{%iAq##M8r4-SQe|yF78h~-3wW<;euS>sn_4zqx`0(R6fkZnmC#XytUz% zkV3N|kTCzX-8%6lz0zrYgoHc||7fB!)mh!mICJ2=YkXy0fXKwWtlD9aJ_R`H%MtFT zSIQ=z&po+>g2O5)PLRea>Za1^*8cH`W6oPN@DaHB&E;mj7O?gV%j3_AV9{-2{X)ao zd>+UZs7qeuf8oINmy@Xe(~_j|yWjIW<+SUr6FCw@`uuvxB3-AQ4E5LWdrWta5dSM9 zd6VPb4Lq^k5zM}QJgM&2kDKlxh3Q(>ONx0BOESeX(k3%Ncp}SduYZ3{m}E_%J)N7JeDslV^68VdRp{PoX4@>y&>yxF%aorm<3PuiVWLO9+Vtp5y zs)j~tK*URJF1A60z98)VOOGrT4i3<+!sIgr^mOKM^lXv%H9pt zjtlC%H0J)0&;jrrQ}CjVc{g`-U?0vi`wXT_Yz!4qBtrVcJ~xKE2W7RJfdsZ_qKs&b z6jG7N0T2SvOEH-zQ@~{Q<;#zqqctW6CCKzjFv*EH*1Byi=pelF7r1QJs`+d*mIUjky^)jq? z8W-TM;UWx2zB&byQ+x+#&1l>=F6X1%a;~nfo9_*?K5Gg-okq!th=>r_y(`V?z7f_v z<>~PROzpD}B`@-t=$f~I`vCM^?t$E(i8z7rz2O!N5XGKAi0p@Jfa~|jX#nJ5GPs*e zG8u>_E1+Fyr+=^%@r7xQp-6;0AN9ec zo8i0~@cHTF`3INVS^&-uuJem2rPN5m#pxI7cd~0394w!8gIFM%fb9gH zIKK%$#bXFq9yk3Z6NSt6d7;b;?19c>GRM=UWj$r(n2`(t9Uq{0;a7tNO01I4S)3%P z(9K^cJZ{^xQ@%`4)L{Javk72`(Hl$uy%hj*P>o?XjPbWJ)^(SEk_Mov*p-+J}c$f3(2K;}e z!Afh-BwHaJ)BdRM%^8z3KZh<<&u8ryIEKir@1a#IF548C0~n- zywXSY6IXhI6}l1Or|=)|-@ahB=|l}Dgy3a{dC`c2Zp~HB96{qgBr;0;%_4L?;Z)Hj z34YN+v~=EOsZ*r0tylUKT+SmOt)V)_3x78#r)mqEC$3AYq=U1I#t=-*Y>(s++=q`G zT=vBJji^*&?(mTs)8WkngXd-s;>up*ms`z3 zv>|=VB6_3FDERtUa%YRg4UY9n^Nf4+yi#3|Oz>M9I3oU6A}uf2dVWuE^(#ofn;D*^ zsQc@w35@YSqsB7ciPZfBUhEZJ*d{L8NXmr0M&jIBleSz#%&|Z0qM2~T>ukX*$!x~q zwAp46*Bzj&so)<>wiYfnj?gS8kzD^ZXw4TXg{;cXs&djt6L|^f~ zUIE{1XCHC-UuUdhDt$04xkC#Tjk+}^xz{v#!0Ae(wf;ti$7vZapT;u*N8R9YzKv&x z-Yr~3g7tfT9R@Lc>?M-B0Q$cl0LGphX2`-hn&P_#oN<8eIw)f?L!kMHcMm#Krd1mj z*&-m~P}O3ZBsS1LPkEiTlkbbF0?mSHVS3Qg%W(|6Cc;A`I`Q~?!H*6t)y>~>r0<~W ziiloKh?>$NIFZ`E6pe%z!garjfzZHwd{ zj*LghseP-CTmfpj;4@N|4aG??E5jl~4sqt~h#7ghkmw!71#UObTbOJGchi6CpXEQQ z-2p;wYIVrKjtCk{+l9*e-z05zaNC*B4`#-yU?rtSAL#VgIm0@0T|6Td$r{64E1f|NHvWMF1N8 zBH+Ls5qU^%qEFC?jp19o##-hn&4J&Gy7+1c*@1cO56A1|wJRW2^mmWUx5?-6ZonR) zmH0cHi0IGzmx7#6D`tfwZwBy;Mkx9?B?<8Pzv06SBGjaxWBuhpbL}MD$hG@X5j8JD*SLnX|=F1ZKqaSz)qmhif z4f#nz7I{MuxPCUw!rhlcA@*%ft6EeVD~_H5HdqwzLr`_p{9&B~_5xFq>Q-ro%3b{=}?oM8LgoRN;T=ww}yx!7zDdlJ!tE}is?2CL|erZGXIaXvkHhK z>b7(eoZ#;6?h@P~xI^&Z?$%fc4vo9J6A11QAT;joA-KESRQ@{;^Kc*L&b(4ocXio0 zXYaMXB|(c;_>*7EkuZPQD-iE%B=2av%^e}Kcm>_I7++<54)3k` zK(0HiVUUgPyV{pS?QWaMKs-cBrOBMhA#oXGDheAktp?q23P}u8SM-aYCVt{-=(~76 zY2#p&4k@Iba7)sQ825$;`R%Ycp9T{X@q&YxR7-&v9Iu>j*{s0#f6p-n*;<7d48+H+ z2Y&}{P$D1{;(su?3M#^pR+0xa`bDL3pxF3CfJ#9+?IsdPns8tr+t2$f+IKI0WCb67kL ze2F;rOEh~g-Bm#kWt8hbu_NIccYa0k(CjP&qK`z$I< zt1YXVp{`zKwU3k_YNrLgFJG+qWOLz8W}BA0QnCAN$zy=piQQJwL=cZdYM1=3py@PX zq0oU&2$2ZvswYr*KF69;nljA(%|S91Yr@Zd-CK)3tzBy`2zqEY!x}8YgzP7RN%;K3 zfjSEgeVj1D@;@uKb?KUJEaq8Y9uGHnZFHh=g#A{yLs75z0X_RKQdl?<_}}P4Kvnw# zykCJ(fkA=aa*mr|GH9x%_RJC}Z6mBe{#hys(dFiCH00Gjx4ROhb;goH-%V9Y>&)Gb z^9!KxPvr@HttTN_{@OrC^WA>SgLM3b;%hIDk`h-_H0511+S^6bMVIvSV@K_8bP*&F z^L`ItEz5jjBWiMM5insA|NDNejrqkSh1cWZkg&Zr|ANWB|L39of87+ zUvg?6^+T^7NW+3A4}ag?0Gm-Xq>Y>KTeR}kxBQ6`Xy-+|yTwjO+f_>CQZ6y`s@J3x zO0a3-E)HhTTPOehb4Z)g2}eRCLvPj4QRiP;n_F()eUSeJOh&P)J>X+pR&cB~E7_^g zFHy2!F$Z=M(Xy9sZFtPQ2^&VQxwVRkAeRDuR19gVmwubKU_y0F0qZ1%;*Z9^3uXFB zU%Fb>NaSGD#POiSa%UU0%2M&--XA+c$62uO4s9MGs~A63TbJBob{ zH=Ta!3in=U*0LpGC(~VjznL2aum{dCT=?Q#gHMoQWBBQYl%_vYuHIW16^1Iq!Aq0? z4Y`6q(Kf~E+Wrlyu~%5RTRH>akz`Pcf2VY3riKA{l#9?Sw50rGf@&a==LzXWI1me? zApBBzlcHvz*G8L|hYtzdbm=m#r{a`j0>9_fnli0W22MwooyunX=1@ZDd|S~tH_Z>W zze&wD4(!nYwI}!*Ax`(Yr4s|biOM~N-CGFagd~s*n+$|7)@^xskC`!w$6ox#)W}7z zZi^cK_y73D3FP1&s+`=6Q=ESk?El^8`)`+s|J4Hg?>8+D9E{hE3^i%E8i}l9`uWoS zy~20_cU=>@yD<&;fFn9Y@T=unme&0`#2=xX(BTS8)}vnq=8yDQQlWz`l$VYUZAdze zXJ6Y}etmaV!UezlzWQ?IU9BYYbLK^|EB9#esPHYuz3bBxa>0ai>E7gM1ooSE1l*N( z&+l<}uJOCj({`$L7<1F?Adi7PUq3dyt6H7`+_1Hn8}D76<;G%YNoL4h__~p0H|sj# z#6$RNm(UVP)|35A^@^{w3oDKD|f3#wJYE-QyRiwuiCZf!&YNuMnK@jOPV^3$jK3j~V>vTa+m#=TA^|KpY3pfUU4R z>6z8hGpD34v%j^2O+2K{!rH-(9rzr@=pWO|$F`=N#`;y{^{}jU-J&Wyj>{B4WTpLY z$iiV_%I+=U?OIVT{=V6VopG>3*d8Ss+eg=dAoV}?wzXfsA~pDz{?KcR6!{bNu&(pH z$#cWs+S>X=#cAbLI^Q(*CJkn5~j$G?y*|9Yop*9FfYRk%cp03gxb=-X9ke@JZAOH5MtFBH7b98Le09$Gur;aZF{{M9e05Vk<%veRd-DUY7dLPbt|BdwzH{1X}K?L+P-7{4+BA{ zBlgV!a)=*4t`M0@m=@|<@zB!Jx+t)9ICk8Ig7JB^#s&85X2y@bo5i2TmpzM{N_eJQ z^;{qrx_6tOtnIA8Sy}ZHLh^N-oMT4q9%m^sq~5*0DeUKgDsUsOC0=&x-&>{ zSmbPB92czMiUlQ6nwf)|^}-Mfax8NM z64YMrN-TNl0^$T8&kJ2vw7aUla<1Y@!ovi!&vGISuQ=AklS_mR4)&s#)Go}<4Rb29 zrm8CoY1YkfL_0Jd_LI(dWE}M}7fMGr1(b>DF=q1ErA+vr&VbN;M)$mDI^wsk4q%#W z=4|WyR#4KpybIX%& zf0pg-tl|L6`FY5(%@s^WW~TX8Xf%f(B;d1!1!#8R<7bd`n+JtM++l{z!@6fTtI-=| zftr?4CPOb{Tj1?%W5+|N8A!oX-SQlzBT*xNU2_az)m>UjC=Mlr-D5QBRCIwg%Y;I_ ze<4TJ70*UU?Yc=)$E`Fm#9ES(_0fkUw%#cRs7lOXow^VS_;)mm35AKgUN`_+;chyh zi*^FW?vkhkS;Q;B2Fu;G-exym#>eWfZgSX;U$vS(DLtGx`a2@E3>>i!TvsD5-0dJ2gprR809`e9w;H4zlxBO5~`RI&FmviwSB=RY(& z%rk}MWeo&(vJHSu1zonoeb0Nsb~{;K*h>^LF(QNSzX+UUZtDC?mu$tJ7)iIx0;X6S z1Ye5bx?6?L?uAKGwg2IDTHp2}EzO4xO4G-UOg7Ia*=^JOEL{$owQ#pia1- z#T;PKYPZ^=3v+LG1r$((%85%aTy8YALt+?-rpO~COa$eEJ37C0{Jeze{Kl~}V$?OJ zFUi`~qSk-G-g+Z%H2aaw3_5maRKFyjbduz6^_oB>eF#u^L>wD)a3%MIW~uIY z4+R#JQEqQ7*ECdD&Xe|eG@Vn44!-*)7{chJuszyO?J~}9IrM;=Rnxm4-!wARLP27z zz0qQbEV|9AO~?Bearw-1G%AbFx}f!!=}zGA&6C4&gYKs%pqJ`MlDcyIaUkmIFky0| z<;;Wli7EIDD667l-@#l?F;=s|9w-Mq*8f8|xtx0MNgLw&hjlId*j%!B=+1rnTvE|9 zOdK}@iGr~AQ4%A(4?m@A5tq@)&ykk79makT)Rt^e;NwC&q)K~h|GKj-&guapL2lxe zO4@#5y4*BfT-+!nKoGW5jBCV`jR^lDY%g)*>L#|;lV6VwAKEFVJ((pifE)g4Ll!1{ zG*@JTNGCCe&{wI%gAFJH30@gY%6Z+)X)asKMfv77G;qyK^*#u@Y?mFlA>niARM;Mw zZ9YFdcJN*j>)oW?R?38(Q{%C_1h=krcvte51O~xoOdMgX8F+h#bU8*vs%HB`$aK5D zhJY5rw#y=!d$TcKg#p4DmtJD%A0zJvP5!52PV?c}@)CQP-T_SjB!P{+qsHd-vo%zK zPWuf=k)G8r74SIb+7Mx7<(-``oJE~Vd^;obl)Y9)*pym)+1hO_VwD0#1{Of_lB`;C(y@7wnUieGj(|vx5mP zml#A@yPqRlWnE{3B6y@*-g%s@Q$C#S5S}4-UVcSZeckl*%uum4@ar_>x{-GJY66P4 zxv{z72F9~sB$Yn@m6PUSr~q$K;n$(eBBw?88;(``x`~#qKi8dOKAi&?d58NwtDdcrISJtXczE8B4qG2$&**G4-jxP+bd^o8Bv(~?ShPj%x zKrLKYfYQ7q??QOW@OUrZUQ~aYLp%xsx+Z!a(IlO7TKRAy+Bh6vSC_Vd+>I_sDyUxh z0f7~69bs8F3|-$%RIplhfRd84FFbkWh=-*5B&*04j*a4W8TROi0GQ2eq%>=FQ>U_Y zY6xJ4K#siM);zf-y$hSh^zA)aSQMesxB?-5@W^EcZ~P$t&=cLD7P! z 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 CPL?wO literal 0 HcmV?d00001 diff --git a/src/main/resources/static/images/ScreenShot/StudyRoomShare.png b/src/main/resources/static/images/ScreenShot/StudyRoomShare.png new file mode 100644 index 0000000000000000000000000000000000000000..69dcdceb947e2bcb8fdac79ed947999fe65fb3d5 GIT binary patch literal 79585 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¥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 RHa-JCigIeQmC|PZ{{yi`$-Dpn literal 0 HcmV?d00001 diff --git a/src/main/resources/static/images/ScreenShot/StudyRoomThumbnail.png b/src/main/resources/static/images/ScreenShot/StudyRoomThumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..14d13341b8e1c2ab4e918ee59f9142eaa935fa0a GIT binary patch literal 121237 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 literal 0 HcmV?d00001 From adc88d08d35089827aca393353ce5b46bd0557d4 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 30 Jun 2026 17:58:56 +0900 Subject: [PATCH 67/67] =?UTF-8?q?[Feat]=20=EB=9E=9C=EB=94=A9=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=8A=A4=ED=84=B0=EB=94=94=EB=A3=B8=20?= =?UTF-8?q?=EC=84=B9=EC=85=98=20=EA=B0=9C=EC=84=A0=20-=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=B0=EC=83=B7=20=EB=B0=B0=EC=B9=98=20=EA=B0=A4?= =?UTF-8?q?=EB=9F=AC=EB=A6=AC=20=EA=B0=84=EA=B2=A9=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/templates/home.html | 111 +++++++++++++++---------- 1 file changed, 69 insertions(+), 42 deletions(-) diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index cdb16be0..28d85713 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -153,7 +153,7 @@ .gallery-track { display: flex; - gap: 18px; + gap: 34px; overflow-x: auto; scroll-snap-type: x mandatory; scroll-behavior: smooth; @@ -367,37 +367,61 @@ font-size: .83rem; color: var(--t3); } - /* ── STUDY ROOM CARDS ── */ - .study-cards { + /* ── STUDY ROOM SHOWCASE ── */ + .study-showcase { display: flex; flex-direction: column; - gap: 16px; - max-width: 380px; + align-items: center; + gap: 28px; + } + .study-phone-duo .phone-img { max-width: 178px; } + .study-phone-duo .phone-img:last-child { + transform: translateY(34px); + } + .study-cards { + display: grid; + grid-template-columns: 1fr; + gap: 12px; + width: 100%; + max-width: 430px; + margin-top: 28px; } .study-card { - display: flex; + display: grid; + grid-template-columns: auto 1fr; + column-gap: 14px; align-items: flex-start; - gap: 16px; - background: var(--bg2); - border-radius: 18px; - padding: 20px 22px; + background: #fff; + border: 1px solid var(--line); + border-radius: 14px; + padding: 16px 18px; } - .study-card-icon { - font-size: 1.75rem; - flex-shrink: 0; - line-height: 1; + .study-card-kicker { + display: inline-flex; + align-items: center; + justify-content: center; + height: 24px; + min-width: 42px; + padding: 0 9px; + border-radius: 999px; + background: rgba(28, 186, 26, .08); + color: var(--brand-dk); + font-family: "Space Grotesk", sans-serif; + font-size: .72rem; + font-weight: 700; + grid-row: 1 / span 2; } .study-card strong { display: block; - font-size: .94rem; + font-size: .96rem; font-weight: 700; color: var(--t1); - margin-bottom: 4px; + margin-bottom: 7px; } .study-card p { - font-size: .84rem; + font-size: .83rem; color: var(--t2); - line-height: 1.55; + line-height: 1.62; word-break: keep-all; margin: 0; } @@ -413,9 +437,13 @@ .feat-copy .band-p { max-width: none; } .feat-img-wrap .phone-img { max-width: 240px; } .phone-duo .phone-img { max-width: 155px; } + .study-phone-duo .phone-img { max-width: 148px; } .phone-duo { gap: 16px; } .stat-row { justify-content: center; } - .study-cards { max-width: none; } + .study-cards { + max-width: none; + margin-top: 26px; + } .achieve-inner { grid-template-columns: 1fr; @@ -508,6 +536,12 @@

-
- 👥 -
- 스터디룸 개설 -

초대 코드로 친구를 불러 함께 공부하는 공간을 만드세요.

-
+ 01 + 함께 공부 +

친구를 초대해 같은 목표를 공유하고 학습 흐름을 이어가요.

- 🏆 -
- 챌린지 설정 -

문제 등록 수, 복습 횟수 등 목표를 정하고 함께 달성해요.

-
+ 02 + 챌린지 +

문제 등록과 복습 목표를 정하고 멤버들과 함께 달성해요.

- 📰 -
- 학습 피드 -

멤버들의 오답노트·복습 활동을 실시간으로 확인하고 응원해요.

-
+ 03 + 랭킹·리포트 +

멤버별 학습 기록과 주간 성과를 한눈에 비교해요.

-
- 📊 -
- 주간 리포트 -

팀원들의 주간 학습 성과를 한눈에 비교할 수 있어요.

-
+
+
+
+
+
+ 스터디룸 챌린지 화면 + 스터디룸 공유 피드 화면