Skip to content

Conversation

@stoneTiger0912
Copy link
Member

@stoneTiger0912 stoneTiger0912 commented Dec 20, 2025

📝 변경 내용


✅ 체크리스트

  • 코드가 정상적으로 동작함
  • 테스트 코드 통과함
  • 문서(README 등)를 최신화함
  • 코드 스타일 가이드 준수

💬 기타 참고 사항

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • WebSocket 인증 및 사용자 추출 지원 추가
    • 카드 세트 문서의 Redis 기반 실시간 저장/동기화 및 MySQL 백업 서비스 추가
    • 카드 세트 콘텐츠 저장/조회 및 카드 목록 조회 엔드포인트 추가
  • 개선 사항

    • API 응답 형식 표준화(성공/오류 유틸) 적용
    • 전역 API 프리픽스 및 CORS 활성화
    • WebSocket 핑/퐁 및 flush 기반 동기화/유지보수 로직 개선
  • 테스트

    • Redis, YJS, WebSocket, MySQL 관련 통합·단위 테스트 및 실행 스크립트 추가

✏️ Tip: You can customize this high-level summary in your review settings.

@stoneTiger0912 stoneTiger0912 self-assigned this Dec 20, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 20, 2025

Walkthrough

WebSocket 기반 Yjs 협업 기능을 추가하고 Redis 기반 문서 저장소 및 MySQL 지속화 로직을 도입했습니다. 새로운 Cardset 관련 엔티티와 YjsDocumentService, WsAuthGuard, WebSocket 게이트웨이 개선 및 관련 테스트가 추가되었습니다.

Changes

코호트 / 파일(s) 변경 요약
패키지 & 런타임 설정
package.json, src/main.ts
의존성 추가(ioredis, dotenv, socket.io-client) 및 테스트 스크립트 추가. 전역 API 프리픽스 api/v1 설정 및 CORS 활성화(src/main.ts).
인증 관련
src/auth/auth.controller.ts, src/auth/auth.module.ts, src/auth/ws-auth.guard.ts, src/auth/auth.service.ts
WebSocket 인증 가드 WsAuthGuard 추가(핸드셰이크 토큰 검증, client.data.user 설정). AuthModule에 가드 제공/내보내기 추가. auth 컨트롤러 응답을 successResponse로 래핑. 경로 수정(import) 조정.
공통 유틸/데이터 구조
src/common/entities/base.entity.ts, src/common/interfaces/api-response.interface.ts, src/common/utils/response.util.ts
BaseEntity(생성/수정 타임스탬프) 추가. ApiResponse 인터페이스와 successResponse/errorResponse 유틸 추가.
Card 관련
src/card/card.module.ts, src/card/card.service.ts, src/card/card.controller.spec.ts, src/card/card.service.spec.ts
TypeOrmModule.forFeature(Card) 등록, CardService에 Repository 주입 및 findByCardsetId() 추가. 테스트에서 Card repository 모킹 추가.
Cardset 엔티티(신규)
src/cardset/entities/cardset-snapshot.entity.ts, src/cardset/entities/cardset-incremental.entity.ts, src/cardset/entities/cardset-content.entity.ts
CardsetSnapshot, CardsetIncremental, CardsetContent 엔티티 추가(문서 스냅샷, 증분 히스토리, JSON 콘텐츠).
Cardset 모듈·서비스·컨트롤러
src/cardset/cardset.module.ts, src/cardset/cardset.service.ts, src/cardset/cardset.controller.ts, src/cardset/cardset.service.spec.ts, src/cardset/cardset.controller.spec.ts
CardsetService에 DB/Yjs 연동 로직 구현(로드, 변환, 저장, 증분 플러시 등). Controller 경로 변경(card-sets) 및 POST/GET 엔드포인트 추가. 테스트에서 YjsDocumentService·저장소 모킹.
Cardset Manager 변경
src/cardset-manager/entities/cardset-manager.entity.ts
@Unique@Index 데코레이터 제거(인덱스/유니크 제약 해제).
WebSocket 데코레이터
src/decorators/ws-user.decorator.ts
@WsUser() 파라미터 데코레이터 추가(WebSocket 클라이언트의 인증 사용자 추출).
WebSocket 모듈·게이트웨이·서비스·테스트
src/websocket/websocket.module.ts, src/websocket/websocket.gateway.ts, src/websocket/yjs-document.service.ts, src/websocket/websocket.gateway.integration.spec.ts, src/websocket/yjs-document.service.spec.ts
CollaborationGateway에 WsAuthGuard 적용, cardset 기반 join/leave/update 흐름 재설계, ping 설정 추가. Redis 중심의 YjsDocumentService 구현(문서 저장/로드, 증분 저장·디바운스·MySQL 지속화, 클라이언트 등록 관리). 모듈에서 YjsDocumentService 제공/내보내기 및 통합·단위 테스트 추가.
앱 모듈 변경
src/app.module.ts
새 엔티티(CardsetSnapshot, CardsetIncremental, CardsetContent) 등록. TypeORM synchronize를 환경변수 기반으로 변경, dropSchema/migrationsRun 옵션 추가.

Sequence Diagram(s)

sequenceDiagram
    participant Client as WebSocket 클라이언트
    participant Gateway as CollaborationGateway
    participant Guard as WsAuthGuard
    participant AuthSvc as AuthService
    participant YjsSvc as YjsDocumentService
    participant Redis as Redis
    participant DB as MySQL

    Client->>Gateway: 연결 및 토큰 전송
    Gateway->>Guard: canActivate 호출
    Guard->>AuthSvc: token 검증
    AuthSvc-->>Guard: UserAuth 반환
    Guard-->>Gateway: 접근 허용 (client.data.user 설정)

    Client->>Gateway: handleJoinCardset(cardsetId)
    Gateway->>YjsSvc: loadDocument(cardsetId)
    YjsSvc->>Redis: 문서 조회
    alt Redis에 문서 있음
        Redis-->>YjsSvc: 바이너리 문서 반환
        YjsSvc->>YjsSvc: Y.Doc 복원
    else Redis 비어있음
        YjsSvc->>DB: CardsetContent 조회
        DB-->>YjsSvc: JSON 콘텐츠
        YjsSvc->>YjsSvc: Y.Doc 생성/적용
        YjsSvc->>Redis: 문서 저장 (TTL)
    end
    YjsSvc-->>Gateway: Y.Doc 전달
    Gateway-->>Client: sync(문서) 전송

    Client->>Gateway: handleUpdate(update)
    Gateway->>YjsSvc: saveUpdate(cardsetId, update)
    YjsSvc->>Redis: 문서에 업데이트 적용 및 history 리스트에 저장
    YjsSvc->>YjsSvc: MySQL 지속화 예약(debounce)
    YjsSvc-->>Gateway: 완료
    Gateway->>Gateway: 모든 클라이언트로 broadcast sync
    Gateway-->>Client: sync 전파
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • dungbik

Poem

🐰 토끼가 속삭여요:
Redis에 꿈을 묻고, Yjs로 춤추네,
소켓의 손잡이 잡고 함께 그려요,
카드 하나씩 빚어 동기화의 빛,
협업 숲에 꽃이 피었네 ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.06% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목 'Feat: FN-165 카드셋 관리'는 카드셋 관리 기능 추가라는 일반적인 내용을 포함하고 있으며, 실제 변경사항(Redis 연동, WebSocket 인증, 엔티티 추가, 문서 동기화 등)의 주요 부분을 반영합니다.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

🧹 Nitpick comments (20)
websocket-test.html (1)

7-8: CDN 스크립트에 무결성 해시(SRI) 추가를 권장합니다.

테스트 페이지이지만, CDN에서 로드하는 스크립트에 integrity 속성을 추가하면 스크립트 변조를 방지할 수 있습니다.

🔎 예시
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js" 
        integrity="sha384-..." 
        crossorigin="anonymous"></script>
src/websocket/yjs-document.service.spec.ts (1)

9-15: Mock 객체의 타입을 명시하세요.

repositoryMockany 타입으로 선언되어 타입 안전성이 저하됩니다.

🔎 제안하는 수정 방안
+import { Repository } from 'typeorm';
+
 describe('YjsDocumentService', () => {
   let service: YjsDocumentService;
-  let repositoryMock: any;
+  let repositoryMock: Partial<Repository<CardsetIncremental>>;

   beforeEach(async () => {
     repositoryMock = {
       create: jest.fn(),
       save: jest.fn(),
     };
src/app.module.ts (1)

34-36: synchronize 조건문의 가독성 개선을 고려하세요.

현재 조건문이 이중 부정을 포함하여 의도를 파악하기 어렵습니다.

🔎 제안하는 수정 방안
-      synchronize: process.env.DB_SYNCHRONIZE === 'true' || process.env.NODE_ENV !== 'production',
+      synchronize: process.env.NODE_ENV !== 'production' && process.env.DB_SYNCHRONIZE !== 'false',

또는 더 명확하게:

+      // 개발 환경에서만 자동 스키마 동기화 활성화
+      synchronize: process.env.NODE_ENV === 'development',
test-websocket-methods.ts (3)

14-14: 전역 가변 상태 사용을 피하세요.

전역 변수 client를 사용하면 테스트 간 격리가 제대로 이루어지지 않을 수 있습니다. 각 테스트 함수가 자체 클라이언트를 생성하고 있지만, 전역 변수에 할당하는 것은 불필요하며 혼란을 야기할 수 있습니다.

🔎 제안하는 수정 방안

각 테스트 함수에서 로컬 변수로만 클라이언트를 관리하도록 변경:

-let client: Socket | null = null;
-
 // 유틸리티 함수
 function createClient(): Socket {

그리고 각 테스트 함수 내에서:

 async function testJoinCardset() {
   console.log('\n📋 테스트 1: join-cardset 메서드');
   console.log('=' .repeat(50));
 
   const cardsetId = 'test-join-1';
-  client = createClient();
+  const client = createClient();

220-225: 고정된 지연 시간 대신 이벤트 기반 대기를 사용하세요.

Line 225의 고정된 500ms setTimeout은 테스트를 불안정하게 만들 수 있습니다. 서버 처리가 500ms보다 오래 걸리면 테스트가 실패할 수 있고, 빠르면 불필요하게 테스트 시간이 늘어납니다.

가능하다면 서버에서 leave-cardset 완료를 알리는 응답 이벤트를 추가하고, waitForEvent를 사용하여 대기하는 것이 좋습니다. 또는 타임아웃을 줄이고 문서화하세요.


316-319: TEST_TOKEN 누락 시 테스트를 중단하는 것을 고려하세요.

TEST_TOKEN이 설정되지 않았을 때 경고만 출력하고 계속 진행하면, 인증이 필요한 환경에서는 모든 테스트가 실패하여 혼란을 야기할 수 있습니다.

인증이 필수인 경우 테스트를 조기에 중단하거나, 환경 변수 SKIP_WS_AUTH를 확인하여 조건부로 처리하는 것이 좋습니다:

if (!TEST_TOKEN && process.env.SKIP_WS_AUTH !== 'true') {
  console.error('❌ TEST_TOKEN이 필요합니다. (SKIP_WS_AUTH=true가 아닌 경우)');
  process.exit(1);
}
test-yjs-integration.ts (1)

84-89: 테스트 데이터 정리를 finally 블록으로 이동하는 것을 고려하세요.

현재 정리 코드가 테스트 본문에 있어서, 이전 단계에서 오류가 발생하면 Redis에 테스트 데이터가 남을 수 있습니다.

🔎 제안하는 수정 방안

정리 로직을 finally 블록으로 이동:

    console.log(
      `   ✅ 클라이언트 해제 성공! (활성 클라이언트: ${countAfter}명)\n`,
    );

-   // 5. 정리
-   console.log('5️⃣  테스트 데이터 정리...');
-   await redisClient.del(key);
-   await redisClient.del(historyKey);
-   await redisClient.del(cardsetKey);
-   console.log('   ✅ 테스트 데이터 삭제 완료!\n');
-
    console.log('🎉 모든 통합 테스트 통과!');
  } catch (error) {
    console.error('❌ 오류 발생:', error);
    if (error instanceof Error) {
      console.error(`   메시지: ${error.message}`);
    }
    process.exit(1);
  } finally {
+   // 테스트 데이터 정리
+   console.log('5️⃣  테스트 데이터 정리...');
+   try {
+     await redisClient.del(key);
+     await redisClient.del(historyKey);
+     await redisClient.del(cardsetKey);
+     console.log('   ✅ 테스트 데이터 삭제 완료!\n');
+   } catch (cleanupError) {
+     console.warn('⚠️  정리 중 오류:', cleanupError);
+   }
    redisClient.disconnect();
    console.log('\n🔌 Redis 연결 종료');
  }
src/cardset/entities/cardset-content.entity.ts (1)

22-24: 관계 설정 검토 필요

OneToOne 관계가 단방향으로 정의되어 있습니다. 다음 사항들을 확인해주세요:

  • Cardset 엔티티에 역방향 관계(cardsetContent)가 필요한지 검토
  • 데이터 삭제/업데이트 시 cascade 옵션(예: cascade: ['remove']) 필요 여부 검토

양방향 관계가 필요한 경우, Cardset 엔티티에도 관계를 추가하고 cascade 동작을 명시적으로 정의하는 것을 권장합니다.

🔎 양방향 관계 설정 예시

CardsetContent에서:

-  @OneToOne(() => Cardset, { createForeignKeyConstraints: false })
+  @OneToOne(() => Cardset, (cardset) => cardset.cardsetContent, { 
+    createForeignKeyConstraints: false,
+    onDelete: 'CASCADE'
+  })
   @JoinColumn({ name: 'cardset_id' })
   cardset?: Cardset;

Cardset 엔티티에 추가:

@OneToOne(() => CardsetContent, (content) => content.cardset)
cardsetContent?: CardsetContent;
src/websocket/websocket.gateway.integration.spec.ts (2)

90-104: 타입 안전성 개선 제안

테스트 코드에서 user as any를 사용하고 있습니다. 테스트의 가독성과 타입 안전성을 위해 테스트용 UserAuth 객체 또는 픽스처를 정의하는 것을 권장합니다.

🔎 개선 예시
+  // 테스트 픽스처
+  const createTestUser = (userId: string) => ({
+    userId,
+    email: `${userId}@test.com`,
+    // UserAuth의 다른 필수 필드들...
+  });
+
   it('should load document from Redis when available', async () => {
     const cardsetId = 'test-join-1';
-    const user = { userId: 'user-1' };
+    const user = createTestUser('user-1');
     
     // ...
     await gateway.handleJoinCardset(
-      user as any,
+      user,
       mockSocket as Socket,
       { cardsetId },
     );

13-192: 테스트 커버리지 확장 권장

현재 테스트는 주요 happy path를 잘 커버하고 있습니다. 다음 시나리오들에 대한 테스트 추가를 권장합니다:

에러 시나리오:

  • Redis/DB 연결 실패
  • 잘못된 cardsetId (존재하지 않는 cardset)
  • 손상된 Yjs 업데이트 데이터

Edge cases:

  • 동시 다중 클라이언트 접속
  • 빠른 연속 업데이트
  • 대용량 문서 처리

누락된 플로우:

  • leave-cardset 동작 (클라이언트 등록 해제, 문서 저장 트리거 등)
src/cardset/cardset.controller.ts (1)

9-15: HTTP 메서드 선택 검토

POST /:cardSetId 패턴이 사용되고 있습니다. 일반적인 RESTful 규칙에서:

  • POST는 리소스 생성에 사용
  • PUT/PATCH는 기존 리소스 업데이트에 사용

현재 구현은 Redis의 데이터를 DB에 저장하는 작업으로 보입니다. 만약:

  • 기존 cardset 내용을 업데이트하는 것이라면 → PUT 또는 PATCH 사용 권장
  • action/command를 트리거하는 것이라면 → 현재 POST 사용이 적절하나, 엔드포인트를 /card-sets/:cardSetId/save 같이 명시적으로 변경 고려
src/cardset/cardset.service.ts (1)

11-211: 서비스 책임 분리 고려

현재 CardsetService가 여러 책임을 가지고 있습니다:

  • DB 접근 및 영속성 관리
  • Yjs ↔ JSON 변환
  • 비즈니스 로직

선택적 개선 사항:

  1. Yjs 변환 로직 분리: YjsConverter 또는 CardsetTransformer 같은 별도 클래스로 추출하여 재사용성과 테스트 용이성 향상
  2. 로깅 수준 조정: 프로덕션에서는 debug 레벨로 전환하여 성능 영향 최소화
🔎 리팩토링 예시 - 변환 로직 분리
// cardset-transformer.service.ts
@Injectable()
export class CardsetTransformer {
  jsonToYjsDoc(jsonContent: any): Y.Doc {
    const doc = new Y.Doc();
    if (Array.isArray(jsonContent)) {
      const cardsArray = doc.getArray('cards');
      jsonContent.forEach((card) => {
        const cardMap = new Y.Map();
        cardMap.set('id', card.id || randomUUID());
        cardMap.set('question', new Y.Text(card.question || ''));
        cardMap.set('answer', new Y.Text(card.answer || ''));
        cardsArray.push([cardMap]);
      });
    }
    return doc;
  }

  yjsArrayToCards(cardsArray: Y.Array<unknown>): Card[] {
    // extraction logic...
  }
}

// cardset.service.ts에서 사용
constructor(
  // ...existing deps
  private readonly transformer: CardsetTransformer,
) {}
src/websocket/yjs-document.service.ts (2)

137-140: Y.Doc 객체의 JSON.stringify는 의미 있는 출력을 생성하지 않음

JSON.stringify(doc)은 Y.Doc 객체의 내용을 제대로 직렬화하지 않습니다. Y.Doc은 내부적으로 CRDT 구조를 가지고 있어 JSON.stringify로는 빈 객체나 무의미한 결과가 출력됩니다.

🔎 문서 내용을 로깅하려면 Yjs의 데이터 구조를 직접 추출하세요
-      // Yjs 문서로 변환한 후의 내용 로그
-      const docJson = doc;
-      this.logger.log(
-        `[loadDocument] Cardset ${cardsetId} - Yjs document content: ${JSON.stringify(docJson, null, 2)}`,
-      );
+      // Yjs 문서의 공유 데이터 구조 로그
+      const cards = doc.getArray('cards');
+      this.logger.log(
+        `[loadDocument] Cardset ${cardsetId} - Yjs document cards count: ${cards.length}`,
+      );

280-283: MySQL 저장과 Redis 삭제 간 원자성 부재

MySQL 저장 후 Redis 리스트 삭제가 실패하면 다음 flush 시 동일 데이터가 중복 저장될 수 있습니다. 반대로 중간에 프로세스가 종료되면 데이터 정합성 문제가 발생할 수 있습니다.

🔎 중복 방지를 위한 개선 방안
      // MySQL에 저장
      await this.cardsetIncrementalRepository.save(entities);
-      // 저장 완료 후 Redis 리스트 삭제
-      await this.redisClient.del(historyKey);
+      // 저장 완료 후 Redis 리스트 삭제 (실패 시 재시도)
+      try {
+        await this.redisClient.del(historyKey);
+      } catch (delError) {
+        this.logger.error(
+          `Failed to delete Redis history after MySQL save for cardset ${cardsetId}, may cause duplicates:`,
+          delError,
+        );
+        // 중복 데이터 처리를 위해 MySQL에 unique constraint 또는 
+        // idempotency key 추가 고려
+      }

또는 각 incremental에 고유 ID를 부여하고 MySQL에서 UPSERT를 사용하여 중복을 방지할 수 있습니다.

src/websocket/websocket.gateway.ts (6)

21-23: CORS origin이 모든 출처를 허용함

origin: '*'는 개발 환경에서는 편리하지만, 프로덕션에서는 보안상 특정 도메인만 허용하는 것이 좋습니다.

  cors: {
-    origin: '*',
+    origin: process.env.CORS_ORIGIN || '*',
  },

141-150: awareness 핸들러에서 입력 검증 누락

cardsetIdawareness 데이터에 대한 검증이 없습니다. 잘못된 데이터가 전달되면 예기치 않은 동작이 발생할 수 있습니다.

🔎 기본 검증 추가
  @SubscribeMessage('awareness')
  handleAwareness(
    client: Socket,
    payload: { cardsetId: string; awareness: Uint8Array },
  ) {
    const { cardsetId, awareness } = payload;
+    
+    if (!cardsetId || !awareness) {
+      this.logger.warn(`Invalid awareness payload from client ${client.id}`);
+      return;
+    }

    // 같은 문서에 있는 클라이언트에 "awareness"로 브로드캐스트
    this.broadcastAwareness(cardsetId, awareness);
  }

186-190: 업데이트 발신자에게도 sync 브로드캐스트됨

broadcastSync는 카드셋 룸의 모든 클라이언트에게 전송하므로, 업데이트를 보낸 클라이언트도 자신의 변경사항을 다시 받게 됩니다. Yjs는 중복 업데이트를 처리할 수 있지만, 불필요한 네트워크 트래픽이 발생합니다.

🔎 발신자 제외 브로드캐스트 고려
-      // 업데이트 적용 후 모든 클라이언트에게 sync 브로드캐스트
-      this.broadcastSync(cardsetId, doc);
+      // 업데이트 적용 후 발신자 제외 브로드캐스트
+      const blob = this.createSyncBuffer(doc, cardsetId);
+      client.to(`cardset:${cardsetId}`).emit('sync', blob);

단, 클라이언트 구현에서 자신의 업데이트 확인(acknowledgment)이 필요한 경우에는 현재 방식을 유지하세요.


206-214: Yjs 상태를 JSON 배열로 직렬화하면 페이로드 크기가 크게 증가함

Array.from(state)는 Uint8Array를 숫자 배열로 변환합니다. 예를 들어 바이트 [255, 128, 0]이 JSON으로 [255,128,0]이 되어 3바이트가 ~12바이트로 증가합니다. 대규모 문서에서는 네트워크 오버헤드가 상당할 수 있습니다.

🔎 Base64 인코딩을 고려하세요
  private createSyncBuffer(doc: Y.Doc, cardsetId: string): Buffer {
    const state = Y.encodeStateAsUpdate(doc);
-    // JSON 구조를 직접 문자열로 구성하여 중간 객체 생성 최소화
-    const jsonStr = JSON.stringify({
-      cardsetId,
-      update: Array.from(state),
-    });
+    // Base64 인코딩으로 페이로드 크기 최적화
+    const jsonStr = JSON.stringify({
+      cardsetId,
+      update: Buffer.from(state).toString('base64'),
+    });
    return Buffer.from(jsonStr, 'utf8');
  }

클라이언트에서도 Base64 디코딩이 필요합니다.


371-381: 모듈 종료 시 flushTimeouts 정리 누락

Gateway 클래스는 OnModuleDestroy를 구현하지 않아 모듈 종료 시 flushTimeouts의 타이머가 정리되지 않습니다. YjsDocumentService는 이를 올바르게 처리하고 있습니다.

🔎 OnModuleDestroy 구현 추가
export class CollaborationGateway
  implements OnGatewayConnection, OnGatewayDisconnect, OnModuleDestroy
{
  // ... existing code ...

  onModuleDestroy() {
    this.flushTimeouts.forEach((timeout) => clearTimeout(timeout));
    this.flushTimeouts.clear();
    this.logger.log('Cleared all pending flush timeouts');
  }
}

OnModuleDestroy import도 필요합니다.


281-284: sendAwareness 메서드가 정의되었지만 사용되지 않음

sendAwareness 메서드(lines 281-292)가 private으로 정의되어 있지만, broadcastAwareness만 사용되고 있습니다. 개별 클라이언트에게 awareness를 보내는 경우가 없다면 불필요한 코드입니다.

메서드를 제거하거나, 향후 사용 계획이 있다면 유지하세요.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3665389 and e22a109.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (31)
  • package.json (3 hunks)
  • src/app.module.ts (2 hunks)
  • src/auth/auth.controller.ts (1 hunks)
  • src/auth/auth.module.ts (1 hunks)
  • src/auth/ws-auth.guard.ts (1 hunks)
  • src/card/card.module.ts (1 hunks)
  • src/card/card.service.ts (1 hunks)
  • src/cardset-manager/entities/cardset-manager.entity.ts (1 hunks)
  • src/cardset/cardset.controller.ts (1 hunks)
  • src/cardset/cardset.module.ts (1 hunks)
  • src/cardset/cardset.service.spec.ts (1 hunks)
  • src/cardset/cardset.service.ts (1 hunks)
  • src/cardset/entities/cardset-content.entity.ts (1 hunks)
  • src/cardset/entities/cardset-incremental.entity.ts (1 hunks)
  • src/cardset/entities/cardset-snapshot.entity.ts (1 hunks)
  • src/common/entities/base.entity.ts (1 hunks)
  • src/common/interfaces/api-response.interface.ts (1 hunks)
  • src/common/utils/response.util.ts (1 hunks)
  • src/decorators/ws-user.decorator.ts (1 hunks)
  • src/main.ts (2 hunks)
  • src/websocket/websocket.gateway.integration.spec.ts (1 hunks)
  • src/websocket/websocket.gateway.ts (1 hunks)
  • src/websocket/websocket.module.ts (1 hunks)
  • src/websocket/yjs-document.service.spec.ts (1 hunks)
  • src/websocket/yjs-document.service.ts (1 hunks)
  • test-mysql.ts (1 hunks)
  • test-redis.ts (1 hunks)
  • test-websocket-methods.ts (1 hunks)
  • test-websocket-simple.ts (1 hunks)
  • test-yjs-integration.ts (1 hunks)
  • websocket-test.html (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (12)
src/card/card.service.ts (1)
src/cardset/cardset.service.ts (1)
  • Injectable (10-211)
src/card/card.module.ts (3)
src/app.module.ts (1)
  • Module (15-46)
src/cardset/cardset.module.ts (1)
  • Module (9-18)
src/websocket/websocket.module.ts (1)
  • Module (9-18)
src/common/utils/response.util.ts (1)
src/common/interfaces/api-response.interface.ts (1)
  • ApiResponse (4-9)
src/cardset/entities/cardset-content.entity.ts (2)
src/cardset/entities/cardset-incremental.entity.ts (1)
  • Entity (15-41)
src/cardset/entities/cardset-snapshot.entity.ts (1)
  • Entity (15-30)
src/cardset/cardset.module.ts (3)
src/app.module.ts (1)
  • Module (15-46)
src/websocket/websocket.module.ts (1)
  • Module (9-18)
src/card/card.module.ts (1)
  • Module (7-13)
src/cardset/entities/cardset-snapshot.entity.ts (2)
src/cardset/entities/cardset-content.entity.ts (1)
  • Entity (16-33)
src/cardset/entities/cardset-incremental.entity.ts (1)
  • Entity (15-41)
src/websocket/websocket.module.ts (2)
src/app.module.ts (1)
  • Module (15-46)
src/cardset/cardset.module.ts (1)
  • Module (9-18)
src/decorators/ws-user.decorator.ts (1)
src/types/userAuth.type.ts (1)
  • UserAuth (1-5)
src/cardset/entities/cardset-incremental.entity.ts (2)
src/cardset/entities/cardset-content.entity.ts (1)
  • Entity (16-33)
src/cardset/entities/cardset-snapshot.entity.ts (1)
  • Entity (15-30)
src/auth/auth.controller.ts (3)
src/decorators/auth-user.decorator.ts (1)
  • AuthUser (5-10)
src/types/userAuth.type.ts (1)
  • UserAuth (1-5)
src/common/utils/response.util.ts (1)
  • successResponse (10-21)
src/cardset/cardset.controller.ts (2)
src/card/card.controller.ts (1)
  • Controller (4-7)
src/common/utils/response.util.ts (1)
  • successResponse (10-21)
src/websocket/websocket.gateway.ts (2)
src/decorators/ws-user.decorator.ts (1)
  • WsUser (4-9)
src/types/userAuth.type.ts (1)
  • UserAuth (1-5)
🪛 GitHub Actions: Node CI with NestJS
src/cardset-manager/entities/cardset-manager.entity.ts

[error] 1-1: 'Index' is defined but never used (no-unused-vars)

🪛 GitHub Check: build
src/websocket/websocket.gateway.integration.spec.ts

[warning] 124-124:
Unsafe argument of type any assigned to a parameter of type UserAuth


[warning] 100-100:
Unsafe argument of type any assigned to a parameter of type UserAuth

src/websocket/yjs-document.service.spec.ts

[failure] 33-33:
Unsafe assignment of an any value

🔇 Additional comments (27)
src/cardset-manager/entities/cardset-manager.entity.ts (1)

4-6: 유니크 제약 조건 및 인덱스 제거에 대한 확인이 필요합니다.

userIdcardSetId의 유니크 제약 조건이 주석 처리되면 동일한 사용자가 동일한 카드셋에 중복으로 추가될 수 있습니다. 이는 데이터 무결성 문제를 초래할 수 있습니다.

의도적인 변경이라면, 왜 이 제약 조건이 필요 없는지 주석으로 설명을 추가하거나, 임시 비활성화라면 TODO 주석을 남겨주세요. 완전히 제거할 예정이라면 주석 처리된 코드도 삭제하는 것이 좋습니다.

src/cardset/cardset.service.spec.ts (1)

11-47: 테스트 설정이 적절합니다.

repositoryMockFactory를 사용한 mock 패턴과 getRepositoryToken을 통한 의존성 주입 설정이 NestJS 테스트 모범 사례를 따르고 있습니다. YjsDocumentService mock도 올바르게 구성되어 있습니다.

향후 실제 테스트 케이스를 추가할 때 CardsetService의 주요 메서드들에 대한 테스트를 확장하시기 바랍니다.

test-redis.ts (1)

8-75: 테스트 스크립트 로직이 적절합니다.

Redis 연결, Yjs 문서 저장/로드, 정리 작업의 흐름이 잘 구성되어 있습니다. finally 블록에서 disconnect()를 호출하여 리소스 정리를 보장하는 점도 좋습니다.

package.json (2)

20-26: 테스트 스크립트 추가가 잘 되었습니다.

Redis, Yjs, WebSocket, MySQL 테스트를 위한 개별 스크립트가 잘 구성되어 있습니다. tsconfig-paths/register를 사용하여 경로 별칭을 지원하는 점도 적절합니다.


38-38: ioredis가 dependencies에 추가되었습니다.

프로덕션 코드(YjsDocumentService 등)에서 Redis를 사용한다면 dependencies에 추가하는 것이 올바릅니다. 테스트 전용이라면 devDependencies로 이동을 고려하세요.

websocket-test.html (1)

287-505: WebSocket 테스트 UI 구현이 잘 되어 있습니다.

연결 관리, 카드셋 조인/나가기, Yjs 문서 조작, 이벤트 로깅 등 협업 기능 테스트에 필요한 기능들이 잘 구현되어 있습니다. 에러 처리와 상태 표시도 적절합니다.

src/cardset/cardset.module.ts (1)

1-18: 모듈 구성이 적절합니다.

TypeORM 엔티티 등록, WebSocket 모듈 통합(forwardRef를 통한 순환 의존성 해결), 그리고 CardsetService export가 올바르게 구성되었습니다.

src/main.ts (1)

10-11: 글로벌 API prefix 설정이 적절합니다.

API 버전 관리를 위한 글로벌 prefix 설정이 올바르게 구현되었습니다.

src/auth/auth.controller.ts (1)

11-12: 응답 표준화가 적절합니다.

successResponse 유틸리티를 사용하여 API 응답 형식을 일관되게 표준화했습니다.

src/card/card.service.ts (2)

7-11: Repository 주입이 올바르게 구현되었습니다.

TypeORM의 의존성 주입 패턴을 올바르게 사용하고 있습니다.


13-18: 카드셋별 카드 조회 메서드가 적절합니다.

findByCardsetId 메서드가 정상적으로 구현되었으며, 정렬 순서도 명시되어 있습니다.

src/websocket/yjs-document.service.spec.ts (1)

41-43: 기본 테스트 케이스가 적절합니다.

서비스 정의 확인을 위한 기본 테스트가 존재하며, 상세한 통합 테스트는 별도 파일에서 수행하는 것이 명시되어 있습니다.

src/app.module.ts (1)

8-10: 새로운 엔티티가 올바르게 추가되었습니다.

CardsetSnapshot, CardsetIncremental, CardsetContent 엔티티가 TypeORM 설정에 정상적으로 등록되었습니다.

src/common/interfaces/api-response.interface.ts (1)

1-10: 공통 응답 인터페이스가 적절하게 정의되었습니다.

제네릭 타입을 사용한 표준 API 응답 인터페이스가 명확하게 구현되어, 전체 애플리케이션에서 일관된 응답 형식을 제공할 수 있습니다.

src/auth/auth.module.ts (1)

4-12: LGTM! WsAuthGuard 모듈 통합이 올바르게 구현되었습니다.

WsAuthGuard를 providers와 exports에 추가하여 다른 모듈(특히 WebSocketModule)에서 사용할 수 있도록 한 것은 적절합니다. NestJS 모듈 구조를 잘 따르고 있습니다.

src/common/entities/base.entity.ts (1)

1-24: LGTM! Base Entity 구현이 깔끔합니다.

타임스탬프 필드를 포함한 추상 베이스 엔티티가 잘 구현되었습니다. TypeORM의 @CreateDateColumn@UpdateDateColumn 데코레이터를 사용하여 자동으로 생성/수정 시간을 관리하는 것은 좋은 접근입니다.

src/card/card.module.ts (1)

2-11: LGTM! TypeORM 통합 및 서비스 export가 적절합니다.

Card 엔티티에 대한 TypeORM 리포지토리 설정과 CardService export가 올바르게 구현되었습니다. 다른 모듈(CardsetModule, WebSocketModule)의 패턴과 일관성이 있습니다.

src/common/utils/response.util.ts (1)

1-42: LGTM! 응답 유틸리티 함수가 잘 구조화되어 있습니다.

successResponseerrorResponse 헬퍼 함수가 깔끔하게 구현되었습니다. 타입 안정성을 유지하면서 일관된 API 응답 형식을 제공하는 좋은 접근입니다. 기본값 설정도 합리적입니다.

src/websocket/websocket.module.ts (1)

1-17: LGTM! 순환 의존성 처리가 적절합니다.

forwardRef를 사용하여 WebSocketModule과 CardsetModule 간의 순환 의존성을 올바르게 해결했습니다. TypeORM 통합과 YjsDocumentService export도 적절하게 구성되어 있습니다.

test-yjs-integration.ts (1)

22-25: LGTM! Redis 재시도 전략이 적절합니다.

지수 백오프(exponential backoff)를 사용하되 최대 2초로 제한한 재시도 전략은 합리적입니다. 통합 테스트 환경에서 일시적인 연결 문제를 잘 처리할 수 있습니다.

src/cardset/entities/cardset-snapshot.entity.ts (2)

20-22: 외래 키 제약 조건 비활성화 의도를 확인하세요.

createForeignKeyConstraints: false 옵션이 모든 Cardset 관련 엔티티(CardsetContent, CardsetIncremental)에서 일관되게 사용되고 있습니다. 이는 의도적인 설계로 보이지만, 데이터 무결성 측면에서 주의가 필요합니다.

외래 키 제약 조건을 비활성화한 이유를 확인하고 문서화하는 것이 좋습니다. 일반적으로 다음과 같은 경우에 사용됩니다:

  • 다른 데이터베이스/스키마를 참조하는 경우
  • 성능상의 이유로 제약 조건을 애플리케이션 레벨에서 관리하는 경우
  • 레거시 데이터베이스와의 호환성

실제로 Cardset 엔티티가 존재하지 않는 cardset_id를 참조하지 않도록 애플리케이션 레벨에서 보장하고 있는지 확인하세요.


28-29: LGTM! MEDIUMBLOB 타입 선택이 적절합니다.

Yjs로 인코딩된 바이너리 문서를 저장하기 위해 mediumblob 타입(최대 16MB)을 사용한 것은 합리적입니다. 대부분의 협업 문서 크기를 수용할 수 있습니다.

test-mysql.ts (1)

95-120: LGTM!

각 테이블 조회를 개별 try-catch로 처리하여 한 테이블의 오류가 다른 테이블 테스트를 차단하지 않도록 구현되어 있습니다. 좋은 에러 핸들링 패턴입니다.

src/cardset/entities/cardset-incremental.entity.ts (1)

24-40: LGTM!

증분 데이터 저장을 위한 컬럼 정의가 적절합니다:

  • incrementalValue: Yjs 바이너리 데이터를 위한 blob 타입
  • isFlushed: MySQL 반영 여부 추적을 위한 boolean 플래그 (기본값 false)
src/auth/ws-auth.guard.ts (1)

35-47: LGTM!

토큰 추출 로직이 잘 구현되어 있습니다:

  • socket.io v4의 handshake.auth.token 우선 사용
  • Authorization 헤더로 fallback
  • Bearer 접두사 처리
src/cardset/cardset.service.ts (1)

170-210: LGTM!

getCardsetInCards 메서드가 잘 구현되어 있습니다:

  • 적절한 에러 처리 (try-catch)
  • 누락된 ID에 대한 UUID 생성
  • 상세한 로깅
  • 에러 시 빈 배열 반환으로 안전한 fallback
src/websocket/yjs-document.service.ts (1)

31-54: Redis 클라이언트 연결 대기 없이 초기화됨

onModuleInit에서 Redis 클라이언트를 생성하지만 연결 완료를 기다리지 않습니다. 모듈 초기화 직후 메서드가 호출되면 Redis가 아직 연결되지 않았을 수 있습니다. 다행히 각 메서드에서 !this.redisClient 체크는 있지만, 연결 중인 상태(connecting)에서의 명령 실행은 ioredis가 자동으로 큐잉하므로 동작은 합니다.

다만, 연결 실패 시 에러 핸들러가 로그만 출력하고 재연결을 시도하는데, 이 상태에서 서비스가 정상 동작하는지 모니터링이 필요합니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/common/utils/response.util.ts (1)

3-21: 구현이 깔끔하고 타입 안전성이 잘 지켜졌습니다.

제네릭 타입 T를 사용하여 다양한 응답 데이터 타입을 지원하고, 기본값 설정도 적절합니다. ApiResponse 인터페이스를 정확히 따르고 있으며, 성공 응답에서는 codenull로 설정하여 에러 응답과 명확히 구분됩니다.

선택적 개선 사항: HTTP 상태 코드 유효성 검사

더 엄격한 타입 안전성을 원한다면, 성공 응답에 대한 상태 코드 범위를 제한할 수 있습니다:

 export function successResponse<T>(
   data: T,
   status = 200,
   message: string | null = null,
 ): ApiResponse<T> {
+  if (status < 200 || status >= 300) {
+    throw new Error('Success response status must be 2xx');
+  }
   return {
     status,
     code: null,
     message,
     data,
   };
 }

다만 이는 유연성을 제한할 수 있으므로 현재 구현도 충분히 적절합니다.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e22a109 and ddb41a3.

📒 Files selected for processing (3)
  • src/cardset-manager/entities/cardset-manager.entity.ts (1 hunks)
  • src/common/interfaces/api-response.interface.ts (1 hunks)
  • src/common/utils/response.util.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/common/interfaces/api-response.interface.ts
  • src/cardset-manager/entities/cardset-manager.entity.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/common/utils/response.util.ts (1)
src/common/interfaces/api-response.interface.ts (1)
  • ApiResponse (4-9)
🔇 Additional comments (1)
src/common/utils/response.util.ts (1)

23-41: 에러 응답 헬퍼 함수가 올바르게 구현되었습니다.

에러 응답의 경우 datanull로 고정하고 code를 필수 파라미터로 받는 것이 적절합니다. 기본 상태 코드 400도 일반적인 클라이언트 에러에 대한 합리적인 선택입니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/app.module.ts (1)

34-36: 이전 리뷰에서 지적된 synchronize 설정 문제가 아직 수정되지 않았습니다.

현재 OR 연산자(||)를 사용하는 로직은 프로덕션 환경에서도 DB_SYNCHRONIZE='true'일 때 자동 스키마 동기화를 활성화합니다. 이는 프로덕션 데이터 손실 위험이 있습니다.

이전 리뷰에서 제안된 대로 AND 연산자(&&)를 사용하거나 프로덕션에서는 무조건 비활성화하도록 수정해주세요:

-      synchronize:
-        process.env.DB_SYNCHRONIZE === 'true' ||
-        process.env.NODE_ENV !== 'production',
+      synchronize:
+        process.env.NODE_ENV !== 'production' &&
+        process.env.DB_SYNCHRONIZE === 'true',

또는 더 안전하게:

-      synchronize:
-        process.env.DB_SYNCHRONIZE === 'true' ||
-        process.env.NODE_ENV !== 'production',
+      synchronize: process.env.NODE_ENV !== 'production',
🧹 Nitpick comments (2)
src/app.module.ts (1)

37-38: 스키마 및 마이그레이션 설정이 안전하게 구성되었습니다.

dropSchema: falsemigrationsRun: false 설정은 프로덕션 환경에서 데이터를 보호하는 안전한 선택입니다. 단, 마이그레이션은 수동으로 실행해야 하므로 배포 프로세스에 마이그레이션 실행 단계가 포함되어 있는지 확인하세요.

src/websocket/websocket.gateway.integration.spec.ts (1)

89-166: 테스트 검증 범위를 확대하는 것을 권장합니다.

현재 테스트는 모의 객체 호출만 검증하고 실제 문서 상태나 내용을 확인하지 않습니다. 다음 개선을 고려하세요:

  • 로드된 문서의 실제 데이터 검증 (예: testArray 또는 dbArray의 내용 확인)
  • 에러 케이스 테스트 추가 (잘못된 cardsetId, 인증 실패 등)
  • 동시 접속 시나리오 테스트
  • disconnect/cleanup 동작 테스트
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ddb41a3 and 88a0eff.

📒 Files selected for processing (6)
  • src/app.module.ts (2 hunks)
  • src/common/interfaces/api-response.interface.ts (1 hunks)
  • src/common/utils/response.util.ts (1 hunks)
  • src/main.ts (2 hunks)
  • src/websocket/websocket.gateway.integration.spec.ts (1 hunks)
  • src/websocket/yjs-document.service.spec.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main.ts
  • src/common/utils/response.util.ts
  • src/websocket/yjs-document.service.spec.ts
  • src/common/interfaces/api-response.interface.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/websocket/websocket.gateway.integration.spec.ts (1)
src/types/userAuth.type.ts (1)
  • UserAuth (1-5)
🪛 GitHub Actions: Node CI with NestJS
src/websocket/websocket.gateway.integration.spec.ts

[error] 1-1: Test suite failed to run due to Cannot find module 'src/config/authConfig' from auth/auth.service.ts.

🔇 Additional comments (5)
src/app.module.ts (2)

8-10: 새로운 엔티티 임포트가 올바르게 추가되었습니다.

CardsetSnapshot, CardsetIncremental, CardsetContent 엔티티의 임포트가 적절하게 추가되어 WebSocket 기반 협업 기능을 지원합니다.


26-33: 엔티티 배열이 올바르게 업데이트되었습니다.

새로운 3개의 엔티티(CardsetSnapshot, CardsetIncremental, CardsetContent)가 기존 엔티티와 함께 TypeORM 설정에 적절하게 추가되었습니다.

src/websocket/websocket.gateway.integration.spec.ts (3)

1-13: LGTM: 임포트 구조가 적절합니다.

통합 테스트에 필요한 모든 의존성이 올바르게 임포트되었습니다.


21-87: LGTM: 테스트 셋업이 포괄적입니다.

모킹 구조와 테스트 모듈 설정이 적절하며, 필요한 모든 의존성이 제공되었습니다.


1-1: authConfig 모듈은 존재하며 import 경로가 올바릅니다. src/config/authConfig.ts 파일이 있고, src/auth/auth.service.ts에서 올바르게 임포트되고 있습니다. 이 파일과 관련된 모듈 누락 문제는 없습니다.

Likely an incorrect or invalid review comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/app.module.ts (1)

37-38: 마이그레이션 전략을 문서화하는 것을 권장합니다.

dropSchema: falsemigrationsRun: false 설정은 안전합니다. 그러나 이 설정과 개발 환경에서의 synchronize: true를 함께 사용하면 다음과 같은 상황이 됩니다:

  • 개발 환경: 스키마가 자동으로 동기화됨 (synchronize: true)
  • 프로덕션 환경: 스키마 변경이 자동으로 적용되지 않음 (synchronize: false, migrationsRun: false)

프로덕션 배포 시 스키마 변경을 어떻게 관리할 것인지 명확한 전략을 수립하고 문서화하는 것이 좋습니다. 예를 들어:

  • TypeORM 마이그레이션 파일 작성 및 관리
  • 배포 프로세스에서 마이그레이션 실행 단계 포함
  • 또는 다른 데이터베이스 스키마 관리 도구 사용
src/auth/ws-auth.guard.ts (1)

19-33: 주석 처리된 테스트 코드 정리 권장

이전 리뷰에서 지적된 보안 취약점(|| true)이 주석 처리로 비활성화되어 보안 문제는 해결되었습니다. 다만, 주석 처리된 코드는 향후 유지보수 시 혼란을 줄 수 있으므로 완전히 제거하는 것을 권장합니다. 필요 시 Git 히스토리를 통해 언제든 복원할 수 있습니다.

🔎 제안: 주석 처리된 코드 제거
-  // // TODO: 테스트용 - JWT 인증 임시 비활성화
-  // // 실제 배포 시에는 아래 주석을 해제하고 테스트 코드를 제거해야 합니다
-  // const SKIP_AUTH = process.env.SKIP_WS_AUTH === 'true' || true; // 테스트용: true로 고정
-
-  // if (SKIP_AUTH) {
-  //   // 테스트용 더미 사용자 데이터
-  //   (client.data as { user: unknown }).user = {
-  //     userId: 'test-user',
-  //     email: 'test@example.com',
-  //   };
-  //   this.logger.warn(
-  //     `⚠️  테스트 모드: 인증을 건너뛰고 있습니다 (client ${client.id})`,
-  //   );
-  //   return true;
-  // }
-
   // 1) socket.io v4: client.handshake.auth.token 로 받기
src/websocket/yjs-document.service.ts (1)

164-208: 동시 업데이트 처리 개선 고려

현재 saveUpdate의 read-modify-write 패턴은 동시 업데이트 시 마지막 저장이 이전 업데이트를 덮어쓸 수 있습니다. 실시간 협업 환경에서 다수의 사용자가 동시에 수정할 경우 업데이트 손실 가능성이 있습니다.

향후 트래픽이 증가하면 Redis 트랜잭션(WATCH/MULTI) 또는 Lua 스크립트를 활용한 원자적 업데이트를 고려해보세요. 현재 구현은 일반적인 사용 사례에서는 작동하지만, 높은 동시성 환경에서는 개선이 필요할 수 있습니다.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 88a0eff and bc4fbc2.

📒 Files selected for processing (10)
  • src/app.module.ts
  • src/auth/auth.service.ts
  • src/auth/ws-auth.guard.ts
  • src/card/card.controller.spec.ts
  • src/card/card.service.spec.ts
  • src/cardset/cardset.controller.spec.ts
  • src/cardset/cardset.controller.ts
  • src/websocket/websocket.gateway.integration.spec.ts
  • src/websocket/websocket.gateway.ts
  • src/websocket/yjs-document.service.ts
🧰 Additional context used
🧬 Code graph analysis (3)
src/cardset/cardset.controller.ts (3)
src/cardset-manager/cardset-manager.controller.ts (1)
  • Controller (4-7)
src/card/card.controller.ts (1)
  • Controller (4-7)
src/common/utils/response.util.ts (1)
  • successResponse (10-21)
src/websocket/websocket.gateway.ts (2)
src/decorators/ws-user.decorator.ts (1)
  • WsUser (4-9)
src/types/userAuth.type.ts (1)
  • UserAuth (1-5)
src/websocket/websocket.gateway.integration.spec.ts (1)
src/types/userAuth.type.ts (1)
  • UserAuth (1-5)
🔇 Additional comments (28)
src/card/card.service.spec.ts (2)

2-4: 필수 imports가 올바르게 추가되었습니다.

TypeORM 리포지토리 모킹을 위해 getRepositoryTokenCard 엔티티가 적절히 import되었습니다.


11-21: 리포지토리 모킹 설정이 올바릅니다.

테스트 모듈이 CardService와 모킹된 Card 리포지토리를 정확히 제공하고 있으며, 기본 CRUD 메서드(findOne, save, create)가 적절히 모킹되었습니다.

src/card/card.controller.spec.ts (2)

2-5: 필수 imports가 올바르게 추가되었습니다.

컨트롤러 테스트를 위해 getRepositoryTokenCard 엔티티가 적절히 import되었습니다.


13-23: 프로바이더 설정이 올바릅니다.

CardController 테스트가 CardService와 모킹된 리포지토리를 모두 등록하여 의존성을 정확히 해결하고 있습니다. 이는 card.service.spec.ts와 일관된 모킹 패턴을 따르고 있어 유지보수성이 좋습니다.

src/app.module.ts (2)

34-36: 이전 리뷰에서 지적된 synchronize 로직 문제가 수정되었습니다.

이전 리뷰에서 OR (||) 연산자 사용으로 인해 프로덕션 환경에서도 스키마 동기화가 활성화될 수 있는 위험이 지적되었습니다. 현재 코드는 AND (&&) 연산자를 사용하여 이 문제를 올바르게 수정했습니다:

  • 프로덕션 환경 (NODE_ENV === 'production')에서는 첫 번째 조건이 false이므로 synchronize가 항상 비활성화됩니다
  • 비프로덕션 환경에서는 DB_SYNCHRONIZE가 명시적으로 'true'일 때만 동기화가 활성화됩니다

이는 안전한 설정이며 이전 리뷰의 권장사항과 일치합니다.


8-10: 세 개의 새로운 엔티티(CardsetSnapshot, CardsetIncremental, CardsetContent)는 모두 올바르게 정의되어 있습니다.

  • ✅ 모든 엔티티에 @Entity() 데코레이터가 적용됨
  • @PrimaryGeneratedColumn(), @Column() 등이 올바르게 정의됨
  • ✅ CardsetSnapshot은 @OneToOne, CardsetIncremental은 @ManyToOne 관계 설정됨
  • ✅ 모든 엔티티가 BaseEntity를 상속하고 제대로 export됨
  • @JoinColumn으로 외래키 매핑 완료
src/cardset/cardset.controller.ts (2)

9-15: 메서드명 일관성 개선이 반영되었습니다.

이전 리뷰에서 지적된 메서드명과 서비스 호출 간의 불일치가 해결되었습니다. saveCardsetContent로 통일되어 코드 가독성이 향상되었습니다.


17-21: 카드 조회 엔드포인트 구현이 적절합니다.

엔드포인트 구현이 명확하고 표준화된 응답 형식을 사용합니다. ParseIntPipe를 통한 입력 검증도 적절하게 적용되어 있습니다.

src/auth/ws-auth.guard.ts (1)

37-59: WebSocket 인증 구현이 우수합니다.

토큰 추출 및 검증 로직이 안전하게 구현되어 있습니다:

  • Socket.IO v4의 handshake.auth.token과 HTTP 헤더 방식 모두 지원
  • Bearer 토큰 형식 적절히 처리
  • 인증 실패 시 적절한 로깅과 거부 처리
src/websocket/websocket.gateway.integration.spec.ts (3)

37-123: 통합 테스트 설정이 포괄적입니다.

테스트 모듈 설정이 잘 구성되어 있습니다:

  • 필요한 모든 의존성 모킹 완료
  • Redis 클라이언트 메서드 적절히 모킹
  • 실제 Redis/DB 없이 격리된 테스트 환경 구축

131-208: join-cardset 플로우 테스트가 적절합니다.

이전 리뷰에서 지적된 cardsetId 변환 문제가 해결되었습니다. 현재 테스트는 세 가지 시나리오를 모두 적절히 커버합니다:

  • Redis에서 문서 로드
  • DB에서 폴백 로드
  • 새 문서 생성

210-252: update 플로우 테스트가 올바르게 개선되었습니다.

이전 리뷰에서 지적된 유효하지 않은 Yjs 업데이트 데이터 문제가 해결되었습니다. 현재 테스트는 실제 Yjs 문서 변경을 통해 유효한 업데이트를 생성하고 브로드캐스트 동작을 검증합니다.

src/websocket/yjs-document.service.ts (7)

31-65: 모듈 생명주기 관리가 적절합니다.

Redis 연결 초기화와 정리가 올바르게 구현되어 있습니다:

  • 재시도 전략 포함
  • 연결 상태 로깅
  • 모듈 종료 시 적절한 정리

263-268: Redis 클라이언트 null 체크가 추가되었습니다.

이전 리뷰에서 지적된 null 체크 누락이 해결되었습니다. 다른 메서드들과 일관되게 Redis 클라이언트 초기화 여부를 확인하고 있습니다.


306-322: 문서 삭제 시 히스토리 정리가 개선되었습니다.

이전 리뷰에서 지적된 증분 업데이트 히스토리 키 미삭제 문제가 해결되었습니다. 이제 메인 키와 히스토리 키를 모두 삭제하여 Redis 메모리 누수를 방지합니다.


70-93: 문서 저장 로직이 견고합니다.

Redis 클라이언트 상태 확인, 적절한 TTL 설정(7일), 그리고 저장 실패 시에도 메모리 상의 문서를 계속 사용할 수 있도록 하는 우아한 오류 처리가 구현되어 있습니다.


98-159: 문서 로드 로직이 상세한 로깅을 포함합니다.

문서 로딩 구현이 올바르며, 디버깅을 위한 상세한 로깅이 포함되어 있습니다. 프로덕션 환경에서는 로그 레벨 설정을 통해 출력량을 조절할 수 있으므로 현재 구현은 적절합니다.


327-412: 클라이언트 관리 로직이 효율적입니다.

Redis Set을 활용한 클라이언트 추적, 적절한 TTL 설정(24시간), 병렬 처리를 위한 Promise.all 사용 등 효율적으로 구현되어 있습니다. 오류 처리도 각 메서드에서 적절히 수행되고 있습니다.


215-237: 디바운스 기반 MySQL 저장 패턴이 적절합니다.

증분 업데이트의 DB 저장을 디바운스하여 쓰기 부하를 줄이는 효율적인 패턴입니다. 수동 플러시 메서드(flushIncrementalHistory)도 제공하여 필요 시 즉시 저장할 수 있습니다.

src/websocket/websocket.gateway.ts (9)

19-52: WebSocket 게이트웨이 설정이 적절합니다.

인증 가드 적용, 적절한 ping 타임아웃 설정(60초/25초), 그리고 순환 의존성 해결을 위한 forwardRef 사용이 올바르게 구현되어 있습니다.


68-70: 클라이언트 등록 기능이 비활성화되어 있습니다.

클라이언트 등록 및 플러시 스케줄링 기능이 주석 처리되어 있습니다. 이로 인해 연결 해제 시 getClientCardsets가 빈 배열을 반환하여 자동 저장 로직이 작동하지 않습니다.

TODO 주석으로 보아 의도적인 비활성화로 보이나, 다음 사항을 권장합니다:

  • TODO에 비활성화 이유와 재활성화 계획을 명시
  • 이슈 트래커에 작업 등록하여 추적

72-114: 문서 로딩 및 오류 처리 전략이 견고합니다.

Redis → DB → 새 문서 생성의 3단계 폴백 전략과, 오류 발생 시에도 빈 문서를 전송하여 클라이언트 연결을 유지하는 복원력 있는 패턴이 구현되어 있습니다.


310-317: cardsetId 검증이 추가되었습니다.

이전 리뷰에서 지적된 NaN 검증 누락이 해결되었습니다. 유효하지 않은 cardsetId에 대해 적절한 오류 로깅과 함께 null을 반환하여 잘못된 DB 쿼리를 방지합니다.


153-195: 업데이트 처리 로직이 올바릅니다.

업데이트 데이터 검증, Yjs 문서 로드/생성, 증분 업데이트 적용 및 Redis 저장, 그리고 모든 클라이언트에 브로드캐스트하는 전체 플로우가 적절히 구현되어 있습니다.


206-273: 버퍼 생성 및 전송 헬퍼가 최적화되어 있습니다.

중간 객체 생성을 최소화하고 직접 Buffer를 생성하여 성능을 개선한 구현입니다. 통일된 인터페이스로 sync/awareness/error 전송을 추상화하여 코드 일관성도 향상되었습니다.


141-150: Awareness 처리가 간결하고 적절합니다.

커서 위치나 선택 영역 등의 awareness 정보를 다른 클라이언트에게 브로드캐스트하는 간단하고 효과적인 구현입니다.


350-362: 새 문서 생성 로직이 견고합니다.

Redis 저장 실패 시에도 메모리 상에서 문서를 사용할 수 있도록 하는 우아한 오류 처리가 구현되어 있습니다. 이는 Redis 장애 시에도 서비스 가용성을 유지하는 데 도움이 됩니다.


380-412: 플러시 스케줄링 메커니즘이 효율적입니다.

클라이언트가 없을 때만 DB에 저장하는 지연 플러시 패턴으로 불필요한 DB 쓰기를 줄입니다. 타임아웃 관리와 활성 클라이언트 수 확인이 적절히 구현되어 있습니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/common/utils/response.util.ts (2)

30-41: LGTM! 에러 응답 헬퍼가 잘 구현되었습니다.

에러 코드와 메시지를 필수로 받아 일관된 에러 응답 형식을 제공합니다. 기본 상태 코드 400은 범용적으로 사용하기에 적절하지만, 상황에 따라 404(Not Found), 401(Unauthorized), 403(Forbidden), 500(Internal Server Error) 등 구체적인 상태 코드를 명시적으로 전달하는 것을 권장합니다.


43-47: 파일 끝 공백 라인 정리를 고려해보세요.

파일 끝에 불필요한 빈 줄이 여러 개 있습니다. 코드 스타일 일관성을 위해 제거하는 것을 권장합니다.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc4fbc2 and 817337d.

📒 Files selected for processing (3)
  • src/cardset/cardset.service.ts
  • src/common/interfaces/api-response.interface.ts
  • src/common/utils/response.util.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/cardset/cardset.service.ts
  • src/common/interfaces/api-response.interface.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/common/utils/response.util.ts (1)
src/common/interfaces/api-response.interface.ts (1)
  • ApiResponse (4-9)
🔇 Additional comments (1)
src/common/utils/response.util.ts (1)

10-21: LGTM! 성공 응답 헬퍼가 잘 구현되었습니다.

제네릭 타입을 활용하여 타입 안전성을 보장하고, 합리적인 기본값(status=200, message=null)을 제공합니다. ApiResponse 인터페이스를 올바르게 구현하고 있습니다.

@stoneTiger0912 stoneTiger0912 merged commit be6ff4d into develop Jan 3, 2026
2 checks passed
@stoneTiger0912 stoneTiger0912 deleted the refactor/cardset-editor/modify branch January 3, 2026 05:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants