Skip to content

Commit 35e8a45

Browse files
Feat: 카드 저장 API 구현
Feat: 카드 저장 API 구현
2 parents 49507ac + 8f331f5 commit 35e8a45

26 files changed

Lines changed: 813 additions & 175 deletions

package-lock.json

Lines changed: 79 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"@nestjs/swagger": "^11.2.0",
3434
"@nestjs/typeorm": "^11.0.0",
3535
"@nestjs/websockets": "^11.1.6",
36+
"ioredis": "^5.10.0",
3637
"jsonwebtoken": "^9.0.2",
3738
"mysql2": "^3.14.4",
3839
"reflect-metadata": "^0.2.2",

src/app.module.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import { CardsetOrmEntity } from './cardset/infrastructure/persistence/orm/cards
1010
import { CardOrmEntity } from './cardset/infrastructure/persistence/orm/card.orm-entity';
1111
import { CardsetManagerOrmEntity } from './cardset/infrastructure/persistence/orm/cardset-manager.orm-entity';
1212
import { CardSetMetadataOrmEntity } from './cardset/infrastructure/persistence/orm/cardset-metadata.orm-entity';
13-
import { YjsDocumentOrmEntity } from './collaboration/infrastructure/persistence/orm/yjs-document.orm-entity';
13+
import { CardsetContentOrmEntity } from './collaboration/infrastructure/persistence/orm/cardset-content.orm-entity';
14+
import { CardsetIncrementalOrmEntity } from './collaboration/infrastructure/persistence/orm/cardset-incremental.orm-entity';
1415

1516
@Module({
1617
imports: [
@@ -27,7 +28,8 @@ import { YjsDocumentOrmEntity } from './collaboration/infrastructure/persistence
2728
CardOrmEntity,
2829
CardsetManagerOrmEntity,
2930
CardSetMetadataOrmEntity,
30-
YjsDocumentOrmEntity,
31+
CardsetContentOrmEntity,
32+
CardsetIncrementalOrmEntity,
3133
],
3234
synchronize: true,
3335
}),

src/auth/infrastructure/guard/ws-auth.guard.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,23 @@ import { Socket } from 'socket.io';
1111
export class WsAuthGuard implements CanActivate {
1212
private readonly logger = new Logger(WsAuthGuard.name);
1313

14-
constructor(private readonly authService: AuthService) {}
14+
constructor(private readonly authService: AuthService) { }
1515

1616
canActivate(context: ExecutionContext): boolean {
1717
const client: Socket = context.switchToWs().getClient<Socket>();
1818

19+
const SKIP_AUTH = process.env.SKIP_WS_AUTH === 'true';
20+
if (SKIP_AUTH) {
21+
(client.data as { user: unknown }).user = {
22+
userId: 'test-user',
23+
email: 'test@example.com',
24+
};
25+
this.logger.warn(
26+
`⚠️ 테스트 모드: 인증을 건너뛰고 있습니다 (client ${client.id})`,
27+
);
28+
return true;
29+
}
30+
1931
const bearer =
2032
(client.handshake.auth?.token as string | undefined) ??
2133
client.handshake.headers?.authorization;

src/cardset/application/cardset.use-case.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { UserGrpcClient } from '../infrastructure/grpc/user-grpc.client';
2121
import type { UserInfo } from '../infrastructure/grpc/user-grpc.client';
2222
import { CreateCardsetRequest } from './dto/request/create-cardset.request';
2323
import { UpdateCardsetRequest } from './dto/request/update-cardset.request';
24+
import { CollaborationUseCase } from '../../collaboration/application/collaboration.use-case';
2425

2526
@Injectable()
2627
export class CardsetUseCase {
@@ -39,6 +40,7 @@ export class CardsetUseCase {
3940
private readonly dataSource: DataSource,
4041
@Inject(CARDSET_METADATA_REPOSITORY)
4142
private readonly metadataRepository: ICardSetMetadataRepository,
43+
private readonly collaborationUseCase: CollaborationUseCase,
4244
) {}
4345

4446
private async checkIsManager(
@@ -58,7 +60,6 @@ export class CardsetUseCase {
5860
async create(userId: number, dto: CreateCardsetRequest): Promise<Cardset> {
5961
await this.groupGrpcClient.checkUserInGroup(dto.groupId, userId);
6062

61-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
6263
const additionalManagerIds: number[] = dto.managerIds ?? [];
6364
for (const managerId of additionalManagerIds) {
6465
await this.groupGrpcClient.checkUserInGroup(dto.groupId, managerId);
@@ -110,11 +111,11 @@ export class CardsetUseCase {
110111
cardSetIds: number[],
111112
): Promise<Map<number, UserInfo[]>> {
112113
if (cardSetIds.length === 0 || this.skipUserGrpc) return new Map();
113-
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
114+
114115
const managers: CardsetManager[] =
115116
await this.cardsetManagerRepository.findByCardSetIds(cardSetIds);
116117
const userIds: number[] = [...new Set(managers.map((m) => m.userId))];
117-
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
118+
118119
const users = await this.userGrpcClient.getUsersByIds(userIds);
119120
const userMap = new Map(users.map((u) => [Number(u.id), u]));
120121
const result = new Map<number, UserInfo[]>();
@@ -249,11 +250,11 @@ export class CardsetUseCase {
249250
}[]
250251
> {
251252
await this.groupGrpcClient.checkUserInGroup(groupId, userId);
252-
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
253+
253254
const cardsets: Cardset[] =
254255
await this.cardsetRepository.findByGroupId(groupId);
255256
const ids: number[] = cardsets.map((c) => c.id);
256-
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
257+
257258
const [metadataMap, likedMap, bookmarkedMap, managersMap] =
258259
await Promise.all([
259260
this.metadataRepository.findByCardSetIds(ids),
@@ -308,7 +309,6 @@ export class CardsetUseCase {
308309

309310
return this.dataSource.transaction(async (manager) => {
310311
if (dto.managerIds !== undefined) {
311-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
312312
const newManagerIds: number[] = dto.managerIds;
313313
for (const managerId of newManagerIds) {
314314
await this.groupGrpcClient.checkUserInGroup(
@@ -404,4 +404,15 @@ export class CardsetUseCase {
404404
return this.cardsetRepository.update(id, updatedCardset);
405405
});
406406
}
407+
408+
async saveCards(cardSetId: number, userId: number): Promise<void> {
409+
await this.checkIsManager(cardSetId, userId);
410+
await this.collaborationUseCase.saveCardsetContent(cardSetId);
411+
}
412+
413+
async findCardsFromYjs(
414+
cardSetId: number,
415+
): Promise<{ id: string; question: string; answer: string }[]> {
416+
return this.collaborationUseCase.getCards(cardSetId);
417+
}
407418
}

src/cardset/application/dto/response/card.response.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,19 @@ export class CardResponse {
3030
res.updatedAt = card.updatedAt;
3131
return res;
3232
}
33+
34+
static fromYjs(card: {
35+
id: string;
36+
question: string;
37+
answer: string;
38+
}): CardResponse {
39+
const res = new CardResponse();
40+
res.id = Number(card.id);
41+
res.content = card.question;
42+
res.order = 0;
43+
res.cardsetId = 0;
44+
res.createdAt = new Date();
45+
res.updatedAt = new Date();
46+
return res;
47+
}
3348
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
3+
export class YjsCardResponse {
4+
@ApiProperty({ example: '1' })
5+
id!: string;
6+
7+
@ApiProperty({ example: '질문 내용' })
8+
question!: string;
9+
10+
@ApiProperty({ example: '답변 내용' })
11+
answer!: string;
12+
13+
static from(card: {
14+
id: string;
15+
question: string;
16+
answer: string;
17+
}): YjsCardResponse {
18+
const res = new YjsCardResponse();
19+
res.id = card.id;
20+
res.question = card.question;
21+
res.answer = card.answer;
22+
return res;
23+
}
24+
}

src/cardset/cardset.module.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { ImageGrpcClient } from './infrastructure/grpc/image-grpc.client';
3030
import { ReactionGrpcClient } from './infrastructure/grpc/reaction-grpc.client';
3131
import { UserGrpcClient } from './infrastructure/grpc/user-grpc.client';
3232
import { GrpcClientModule } from '../shared/grpc/grpc-client.module';
33+
import { CollaborationModule } from '../collaboration/collaboration.module';
3334

3435
@Module({
3536
imports: [
@@ -40,8 +41,14 @@ import { GrpcClientModule } from '../shared/grpc/grpc-client.module';
4041
CardSetMetadataOrmEntity,
4142
]),
4243
GrpcClientModule,
44+
CollaborationModule,
45+
],
46+
controllers: [
47+
CardsetController,
48+
CardController,
49+
GroupCardsetController,
50+
CardsetGrpcController,
4351
],
44-
controllers: [CardsetController, CardController, GroupCardsetController, CardsetGrpcController],
4552
providers: [
4653
{ provide: CARDSET_REPOSITORY, useClass: CardsetRepositoryImpl },
4754
{ provide: CARD_REPOSITORY, useClass: CardRepositoryImpl },

src/cardset/domain/repository/card.repository.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ export interface ICardRepository {
77
findById(id: number): Promise<Card | null>;
88
findAllByCardsetId(cardsetId: number): Promise<Card[]>;
99
save(card: Card, manager?: EntityManager): Promise<Card>;
10-
update(id: number, card: Partial<Card>): Promise<Card | null>;
10+
update(
11+
id: number,
12+
card: Partial<Card>,
13+
manager?: EntityManager,
14+
): Promise<Card | null>;
1115
delete(id: number, manager?: EntityManager): Promise<void>;
1216
updateOrder(
1317
cardId: number,

src/cardset/domain/repository/cardset-metadata.repository.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
export const CARDSET_METADATA_REPOSITORY = Symbol('CARDSET_METADATA_REPOSITORY');
1+
export const CARDSET_METADATA_REPOSITORY = Symbol(
2+
'CARDSET_METADATA_REPOSITORY',
3+
);
24

35
export interface ICardSetMetadataRepository {
4-
findByCardSetId(cardSetId: number): Promise<{ likeCount: number; bookmarkCount: number } | null>;
5-
findByCardSetIds(cardSetIds: number[]): Promise<Map<number, { likeCount: number; bookmarkCount: number }>>;
6+
findByCardSetId(
7+
cardSetId: number,
8+
): Promise<{ likeCount: number; bookmarkCount: number } | null>;
9+
findByCardSetIds(
10+
cardSetIds: number[],
11+
): Promise<Map<number, { likeCount: number; bookmarkCount: number }>>;
612
upsertAndIncrementLike(cardSetId: number): Promise<void>;
713
upsertAndDecrementLike(cardSetId: number): Promise<void>;
814
upsertAndIncrementBookmark(cardSetId: number): Promise<void>;

0 commit comments

Comments
 (0)