Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 관리자 기능 개선 및 추가 #459

Merged
merged 68 commits into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
ffa55d4
refactor: update cron task names and add key-based filtering in getCr…
daechan-jo Sep 13, 2024
1ec8215
Merge branch 'refs/heads/main' into devlop
daechan-jo Sep 13, 2024
5d69992
refactor: update cron task names and add key-based filtering in getCr…
daechan-jo Sep 13, 2024
3e4d3e1
Merge branch 'refs/heads/main' into devlop
daechan-jo Sep 13, 2024
37dabdd
feat: store database implementation
daechan-jo Sep 22, 2024
52cc21e
feat: theme crud
daechan-jo Sep 22, 2024
7266274
feat: item crud
daechan-jo Sep 22, 2024
fa1820b
feat: view and purchase themes
daechan-jo Sep 24, 2024
47d78f7
feat: view and purchase items
daechan-jo Sep 24, 2024
90a8543
feat: activate themes and items
daechan-jo Sep 25, 2024
4c56926
feat: Add layout information to user basic information and modify cac…
daechan-jo Sep 25, 2024
b97c1d1
Merge branch 'refs/heads/main' into devlop
daechan-jo Sep 25, 2024
b6ca049
doc: swagger update
daechan-jo Sep 25, 2024
13bd3b9
doc: swagger update
daechan-jo Sep 25, 2024
e8480d2
doc: swagger update
daechan-jo Sep 25, 2024
efcb945
fix: problem with published posts not being viewed properly due to pa…
daechan-jo Sep 27, 2024
7590068
Merge branch 'refs/heads/main' into devlop
daechan-jo Sep 27, 2024
c6f2853
test: Added console log to resolve issues that occur when using rt
daechan-jo Oct 1, 2024
88e35a8
Merge branch 'refs/heads/main' into devlop
daechan-jo Oct 1, 2024
a6cc1dc
test: Added console log to resolve issues that occur when using rt
daechan-jo Oct 1, 2024
248d4d1
Merge branch 'refs/heads/main' into devlop
daechan-jo Oct 1, 2024
b2a3df2
feat: middleware is added to add request request identifier for testing.
daechan-jo Oct 1, 2024
505c401
feat: middleware is added to add request request identifier for testing.
daechan-jo Oct 1, 2024
de561e5
Merge branch 'refs/heads/main' into devlop
daechan-jo Oct 1, 2024
f4f55c5
fix: replace app.enableCors with express cors middleware for custom h…
daechan-jo Oct 2, 2024
adee23c
Merge branch 'refs/heads/main' into devlop
daechan-jo Oct 2, 2024
e47c450
fix: replace app.enableCors with express cors middleware for custom h…
daechan-jo Oct 2, 2024
a23dc5e
Merge branch 'refs/heads/main' into devlop
daechan-jo Oct 2, 2024
2dfa285
fix: add X-Access-Token to Access-Control-Expose-Headers
daechan-jo Oct 2, 2024
2f1e61b
Merge branch 'refs/heads/main' into devlop
daechan-jo Oct 2, 2024
c6bb957
feat: Add basic theme creation logic
daechan-jo Oct 4, 2024
ccc39d1
Merge branch 'refs/heads/main' into devlop
daechan-jo Oct 4, 2024
3867ee2
refactor: guard refactoring and improving duplicate handling logic
daechan-jo Oct 7, 2024
d2207e1
Merge branch 'refs/heads/main' into devlop
daechan-jo Oct 7, 2024
5fa2731
fix: remove tag limit
daechan-jo Oct 8, 2024
2d6b0a1
Merge branch 'refs/heads/main' into devlop
daechan-jo Oct 8, 2024
344fc4b
refactor: item and theme purchase logic by applying Redis distributed…
daechan-jo Oct 11, 2024
3c718e0
refactor: Add retry logic to method
daechan-jo Oct 11, 2024
d2465ad
refactor: Added name duplication check logic when saving and updating…
daechan-jo Oct 11, 2024
161a2fa
Merge branch 'refs/heads/main' into devlop
daechan-jo Oct 11, 2024
6e7872b
Merge branch 'refs/heads/main' into devlop
daechan-jo Oct 11, 2024
75fb598
fix: 키워드 타입 검사 추가
daechan-jo Oct 14, 2024
d5fe165
Merge branch 'refs/heads/main' into devlop
daechan-jo Oct 14, 2024
30de966
fix: 키워드 타입 검사 추가
daechan-jo Oct 14, 2024
eeabb97
chore: Update dependency package versions to latest
daechan-jo Oct 14, 2024
1131de1
chore: 의존성 패키지 업데이트
daechan-jo Oct 14, 2024
8ed1672
Merge branch 'refs/heads/main' into devlop
daechan-jo Oct 14, 2024
e2e38ef
chore: 의존성 패키지 업데이트
daechan-jo Oct 14, 2024
961ea6d
Merge branch 'refs/heads/main' into devlop
daechan-jo Oct 14, 2024
010158e
chore: 의존성 패키지 업데이트
daechan-jo Oct 14, 2024
bbf13c5
chore: eslint updated
daechan-jo Oct 14, 2024
bee5b9d
fix: redis connection settings in redlock
daechan-jo Oct 14, 2024
d4c1f44
Merge branch 'refs/heads/main' into devlop
daechan-jo Oct 14, 2024
73084ac
fix: 에세이 조회 분산락에 지수 백오프 적용
daechan-jo Oct 18, 2024
428f374
doc: edit readme
daechan-jo Oct 19, 2024
1d0fc52
chore: edit eslint config
daechan-jo Oct 22, 2024
07d4d38
fix: 땅에 묻기 기능 추가로 인한 기존 로직 변경
daechan-jo Oct 22, 2024
9273238
fix: 땅에묻기 필수 좌표 분기 추가
daechan-jo Oct 23, 2024
3159da8
refactor: geometry 타입 변경 밑작업
daechan-jo Oct 23, 2024
aa7cad2
Merge branch 'refs/heads/main' into devlop
daechan-jo Oct 23, 2024
7f16514
fix: 에세이 작성시 위도,경도 데이터 geometry로 변환
daechan-jo Oct 24, 2024
02cfa73
feat: 사용자 위치 기반 주변 에세이 알림 추가
daechan-jo Oct 27, 2024
4bf4f28
Merge branch 'refs/heads/main' into devlop
daechan-jo Oct 27, 2024
912bb01
feat: 사용자 위치 기반 주변 에세이 알림 추가
daechan-jo Oct 27, 2024
7ddbca4
fix: 장치 설정 검색을 일괄 처리하여 성능 최적화
daechan-jo Oct 27, 2024
b97b954
fix: 관리자 검색 API에 대한 페이지네이션 추가
daechan-jo Oct 27, 2024
b1b2ab8
feat: 유저 및 에세이 유사도 검색 추가
daechan-jo Oct 27, 2024
499a338
Merge branch 'refs/heads/main' into devlop
daechan-jo Oct 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/config/typeorm.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export const TypeormConfig: TypeOrmModuleAsyncOptions = {
},
extra: {
max: 9,
keepAlive: true,
idleTimeoutMillis: 30000,
Comment on lines +28 to +29
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Sync connection pool settings between configurations.

The connection pool settings in dataSourceOptions are inconsistent with TypeormConfig. This could lead to different behavior when using direct DataSource vs NestJS TypeORM module.

Apply this diff to sync the settings:

    extra: {
      max: 9,
      connectionTimeoutMillis: 5000,
+     keepAlive: true,
+     idleTimeoutMillis: 30000,
    },

Also applies to: 65-68

connectionTimeoutMillis: 5000,
},
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : undefined,
Expand Down
6 changes: 6 additions & 0 deletions src/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,10 @@ export class User {

@OneToMany(() => SeenRelease, (seenRelease) => seenRelease.user, { onDelete: 'CASCADE' })
seenReleases: SeenRelease[];

@Column({ type: 'tsvector', select: false, nullable: true })
search_vector?: any;

@Column({ type: 'text', nullable: true, select: false })
unaccented_email?: string;
}
52 changes: 52 additions & 0 deletions src/migrations/2024/10/1730047331380-addTrigramUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddTrigramUser1730047331380 implements MigrationInterface {
name = 'AddTrigramUser1730047331380';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS pg_trgm`);
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS unaccent`);

// Add unaccented_email column (no generated expression)
await queryRunner.query(`ALTER TABLE "user" ADD COLUMN "unaccented_email" TEXT`);

// Populate unaccented_email initially
await queryRunner.query(`UPDATE "user" SET "unaccented_email" = unaccent(email)`);

Comment on lines +11 to +15
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Simplify by using a generated column instead of triggers.

Consider using a generated column for unaccented_email instead of manually maintaining it with triggers. This approach reduces complexity and improves maintainability.

Apply this diff to modify the migration:

-await queryRunner.query(`ALTER TABLE "user" ADD COLUMN "unaccented_email" TEXT`);
-
-// Populate unaccented_email initially
-await queryRunner.query(`UPDATE "user" SET "unaccented_email" = unaccent(email)`);
-
-// Create trigger function for unaccented_email updates
-await queryRunner.query(`
-  CREATE OR REPLACE FUNCTION update_unaccented_email()
-  RETURNS TRIGGER AS $$
-  BEGIN
-    NEW.unaccented_email = unaccent(NEW.email);
-    RETURN NEW;
-  END;
-  $$ LANGUAGE plpgsql;
-`);
-
-// Apply trigger to update unaccented_email on email change
-await queryRunner.query(`
-  CREATE TRIGGER trigger_update_unaccented_email
-  BEFORE INSERT OR UPDATE OF email ON "user"
-  FOR EACH ROW EXECUTE FUNCTION update_unaccented_email();
-`);
+await queryRunner.query(`
+  ALTER TABLE "user" ADD COLUMN "unaccented_email" TEXT GENERATED ALWAYS AS (unaccent(email)) STORED
+`);

Don't forget to adjust the down method to drop the generated column:

-await queryRunner.query(`DROP TRIGGER trigger_update_unaccented_email ON "user"`);
-await queryRunner.query(`DROP FUNCTION update_unaccented_email`);
 await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "unaccented_email"`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await queryRunner.query(`ALTER TABLE "user" ADD COLUMN "unaccented_email" TEXT`);
// Populate unaccented_email initially
await queryRunner.query(`UPDATE "user" SET "unaccented_email" = unaccent(email)`);
await queryRunner.query(`
ALTER TABLE "user" ADD COLUMN "unaccented_email" TEXT GENERATED ALWAYS AS (unaccent(email)) STORED
`);

// Create trigger function for unaccented_email updates
await queryRunner.query(`
CREATE OR REPLACE FUNCTION update_unaccented_email()
RETURNS TRIGGER AS $$
BEGIN
NEW.unaccented_email = unaccent(NEW.email);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
`);

// Apply trigger to update unaccented_email on email change
await queryRunner.query(`
CREATE TRIGGER trigger_update_unaccented_email
BEFORE INSERT OR UPDATE OF email ON "user"
FOR EACH ROW EXECUTE FUNCTION update_unaccented_email();
`);

// Create index for trigram search on unaccented_email
await queryRunner.query(
`CREATE INDEX "IDX_USER_UNACCENTED_EMAIL" ON "user" USING gin (unaccented_email gin_trgm_ops)`,
);

// Optional: Create index for text search if required
await queryRunner.query(
`CREATE INDEX "IDX_USER_SEARCH_VECTOR" ON "user" USING gin (to_tsvector('simple', coalesce(email, '')))`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_USER_SEARCH_VECTOR"`);
await queryRunner.query(`DROP INDEX "IDX_USER_UNACCENTED_EMAIL"`);
await queryRunner.query(`DROP TRIGGER trigger_update_unaccented_email ON "user"`);
await queryRunner.query(`DROP FUNCTION update_unaccented_email`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "unaccented_email"`);
}
}
30 changes: 30 additions & 0 deletions src/migrations/2024/10/1730048056148-addVectorUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddVectorUser1730048056148 implements MigrationInterface {
name = 'AddVectorUser1730048056148';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS unaccent`);

// Add search_vector column for full-text search on email
await queryRunner.query(
`ALTER TABLE "user" ADD COLUMN IF NOT EXISTS "search_vector" TSVECTOR GENERATED ALWAYS AS (to_tsvector('simple', coalesce(email, ''))) STORED`,
);
Comment on lines +9 to +12
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider using a more robust text search configuration.

The current implementation uses the 'simple' text search configuration, which:

  1. Doesn't handle language-specific features
  2. Ignores stop words
  3. Doesn't support stemming

Consider using 'english' or another language-specific configuration if the emails contain natural language content.

-      `ALTER TABLE "user" ADD COLUMN IF NOT EXISTS "search_vector" TSVECTOR GENERATED ALWAYS AS (to_tsvector('simple', coalesce(email, ''))) STORED`,
+      `ALTER TABLE "user" ADD COLUMN IF NOT EXISTS "search_vector" TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', coalesce(email, ''))) STORED`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Add search_vector column for full-text search on email
await queryRunner.query(
`ALTER TABLE "user" ADD COLUMN IF NOT EXISTS "search_vector" TSVECTOR GENERATED ALWAYS AS (to_tsvector('simple', coalesce(email, ''))) STORED`,
);
// Add search_vector column for full-text search on email
await queryRunner.query(
`ALTER TABLE "user" ADD COLUMN IF NOT EXISTS "search_vector" TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', coalesce(email, ''))) STORED`,
);


// Check and create index if not exists
await queryRunner.query(`
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'IDX_USER_SEARCH_VECTOR') THEN
CREATE INDEX "IDX_USER_SEARCH_VECTOR" ON "user" USING gin (search_vector);
END IF;
END
$$;
`);
Comment on lines +15 to +23
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Simplify index creation using CREATE INDEX IF NOT EXISTS.

The current DO block implementation is unnecessarily complex. PostgreSQL supports CREATE INDEX IF NOT EXISTS directly.

-    await queryRunner.query(`
-      DO $$ 
-      BEGIN
-        IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'IDX_USER_SEARCH_VECTOR') THEN
-          CREATE INDEX "IDX_USER_SEARCH_VECTOR" ON "user" USING gin (search_vector);
-        END IF;
-      END
-      $$;
-    `);
+    await queryRunner.query(
+      `CREATE INDEX IF NOT EXISTS "IDX_USER_SEARCH_VECTOR" ON "user" USING gin (search_vector)`,
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await queryRunner.query(`
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'IDX_USER_SEARCH_VECTOR') THEN
CREATE INDEX "IDX_USER_SEARCH_VECTOR" ON "user" USING gin (search_vector);
END IF;
END
$$;
`);
await queryRunner.query(
`CREATE INDEX IF NOT EXISTS "IDX_USER_SEARCH_VECTOR" ON "user" USING gin (search_vector)`,
);

}
Comment on lines +6 to +24
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add transaction control to ensure atomic migration.

The migration should be wrapped in a transaction to ensure all changes are applied atomically.

   public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.startTransaction();
+    try {
       // ... existing migration code ...
+      await queryRunner.commitTransaction();
+    } catch (err) {
+      await queryRunner.rollbackTransaction();
+      throw err;
+    }
   }

Committable suggestion was skipped due to low confidence.


public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_USER_SEARCH_VECTOR"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "search_vector"`);
}
Comment on lines +26 to +29
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Enhance down method robustness.

The current down method implementation could fail if objects don't exist and lacks transaction control.

   public async down(queryRunner: QueryRunner): Promise<void> {
-    await queryRunner.query(`DROP INDEX "IDX_USER_SEARCH_VECTOR"`);
-    await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "search_vector"`);
+    await queryRunner.startTransaction();
+    try {
+      await queryRunner.query(`DROP INDEX IF EXISTS "IDX_USER_SEARCH_VECTOR"`);
+      await queryRunner.query(`ALTER TABLE "user" DROP COLUMN IF EXISTS "search_vector"`);
+      await queryRunner.commitTransaction();
+    } catch (err) {
+      await queryRunner.rollbackTransaction();
+      throw err;
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_USER_SEARCH_VECTOR"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "search_vector"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.startTransaction();
try {
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_USER_SEARCH_VECTOR"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN IF EXISTS "search_vector"`);
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
}
}

}
76 changes: 67 additions & 9 deletions src/modules/admin/admin.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import { CreateThemeReqDto } from './dto/request/createThemeReq.dto';
import { CreateItemReqDto } from './dto/request/createItemReq.dto';
import { ItemsResDto } from '../home/dto/response/itemsRes.dto';
import { ThemesResDto } from '../home/dto/response/themesRes.dto';
import { SummaryEssaysResDto } from '../essay/dto/response/SummaryEssaysRes.dto';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Correct the import path to match the file name

The import statement at line 70 may have an incorrect file name in the import path. Ensure that the file name matches the actual file in the directory. If the file is named SummaryEssaysResDto.ts, the import should be adjusted accordingly.

-import { SummaryEssaysResDto } from '../essay/dto/response/SummaryEssaysRes.dto';
+import { SummaryEssaysResDto } from '../essay/dto/response/SummaryEssaysResDto';

Committable suggestion was skipped due to low confidence.


@ApiTags('Admin-auth')
@Controller('admin-auth')
Expand Down Expand Up @@ -436,6 +437,8 @@ export class AdminInfoController {

**쿼리 파라미터:**
- \`activated\`: 어드민의 활성 상태 (true 또는 false, 선택적)
- \`page\`: 페이지 번호 (기본값: 1)
- \`limit\`: 한 페이지에 표시할 신고 수 (기본값: 10)

**동작 과정:**
1. 선택적 쿼리 파라미터 \`activated\`를 기반으로 어드민을 조회합니다.
Expand All @@ -447,8 +450,14 @@ export class AdminInfoController {
})
@ApiResponse({ status: 200, type: AdminsResDto })
@ApiQuery({ name: 'activated', required: false })
async getAdmins(@Query('activated', OptionalBoolPipe) activated?: boolean) {
return this.adminService.getAdmins(activated);
@ApiQuery({ name: 'page', required: false })
@ApiQuery({ name: 'limit', required: false })
async getAdmins(
@Query('page', new PagingParseIntPipe(1)) page: number,
@Query('limit', new PagingParseIntPipe(10)) limit: number,
@Query('activated', OptionalBoolPipe) activated?: boolean,
) {
return this.adminService.getAdmins(page, limit, activated);
}

@Get('inactive')
Expand All @@ -470,6 +479,18 @@ export class AdminInfoController {
return this.adminService.getInactiveAdmins();
}

@Get('my')
@ApiOperation({
summary: '어드민 본인조회',
description: `
어드민 자신의 상세 정보를 조회합니다.
`,
})
@ApiResponse({ status: 200, type: AdminResDto })
async getMyAdmin(@Req() req: ExpressRequest) {
return this.adminService.getAdmin(req.user.id);
}

@Get(':adminId')
@ApiOperation({
summary: '어드민 상세조회',
Expand Down Expand Up @@ -1056,25 +1077,26 @@ export class AdminManagementController {
return this.adminService.getUser(userId);
}

@Get('users/search/:email')
@Get('users/search/:keyword')
@ApiOperation({
summary: '유저 검색 (이메일)',
description: `
관리자 권한으로 이메일로 유저를 검색합니다..

**경로 파라미터:**
- \`email\`: 조회할 유저의 고유 이메일

**동작 과정:**
1. 해당 유저의 상세 정보를 조회합니다.
- \`keyword\`: 검색할 이메일

**주의 사항:**
- 관리자 권한이 필요합니다.
`,
})
@ApiResponse({ status: 200, type: UserDetailResDto })
async searchUser(@Param('email') email: string) {
return this.adminService.searchUser(email);
async searchUsers(
@Param('keyword') email: string,
@Query('page', new PagingParseIntPipe(1)) page: number,
@Query('limit', new PagingParseIntPipe(10)) limit: number,
) {
Comment on lines +1094 to +1098
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Rename the parameter for clarity and consistency

In the searchUsers method, the route parameter is :keyword, but it is assigned to the variable email. For clarity, consider renaming the variable to keyword to match the route parameter. This will improve code readability and maintain consistency.

 async searchUsers(
-    @Param('keyword') email: string,
+    @Param('keyword') keyword: string,
     @Query('page', new PagingParseIntPipe(1)) page: number,
     @Query('limit', new PagingParseIntPipe(10)) limit: number,
   ) {
-    return this.adminService.searchUsers(email, page, limit);
+    return this.adminService.searchUsers(keyword, page, limit);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async searchUsers(
@Param('keyword') email: string,
@Query('page', new PagingParseIntPipe(1)) page: number,
@Query('limit', new PagingParseIntPipe(10)) limit: number,
) {
async searchUsers(
@Param('keyword') keyword: string,
@Query('page', new PagingParseIntPipe(1)) page: number,
@Query('limit', new PagingParseIntPipe(10)) limit: number,
) {
return this.adminService.searchUsers(keyword, page, limit);
}

return this.adminService.searchUsers(email, page, limit);
}

@Put('users/:userId')
Expand Down Expand Up @@ -1105,6 +1127,42 @@ export class AdminManagementController {
return this.adminService.updateUser(req.user.id, userId, data);
}

@Get('essays/search/:keyword')
@ApiOperation({
summary: '에세이 검색',
description: `
키워드를 기반으로 에세이를 검색합니다.

**쿼리 파라미터:**
- \`keyword\`: 검색할 키워드(필수)
- \`page\`: 페이지 번호 (기본값: 1)
- \`limit\`: 한 페이지에 보여줄 에세이 수 (기본값: 10)

**동작 과정:**
1. 주어진 키워드를 제목 또는 내용에서 검색합니다.
2. 검색된 결과에서 페이징 처리를 합니다.
3. 결과는 제목 또는 내용에 키워드가 포함된 에세이의 슬라이스된 내용을 반환합니다.

**주의 사항:**
- 검색 키워드는 URL 인코딩된 문자열이어야 합니다.
- 응답에는 제목 또는 본문에 키워드가 포함된 에세이만 포함됩니다.
`,
})
@ApiQuery({ name: 'keyword', required: true })
@ApiQuery({ name: 'page', required: false })
@ApiQuery({ name: 'limit', required: false })
@ApiResponse({
status: 200,
type: SummaryEssaysResDto,
})
async searchEssays(
@Query('keyword') keyword: string,
@Query('page', new PagingParseIntPipe(1)) page: number,
@Query('limit', new PagingParseIntPipe(10)) limit: number,
) {
return this.adminService.searchEssays(keyword, page, limit);
}

@Get('essays')
@ApiOperation({
summary: '에세이 리스트 조회',
Expand Down
13 changes: 8 additions & 5 deletions src/modules/admin/admin.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,14 @@ export class AdminRepository {
return this.adminRepository.findOne({ where: { name: name } });
}

async findAdmins(activated: boolean) {
if (activated !== undefined) {
return this.adminRepository.find({ where: { activated: activated } });
}
return this.adminRepository.find();
async findAdmins(activated: boolean, page?: number, limit?: number) {
const [admins, total] = await this.adminRepository.findAndCount({
where: activated !== undefined ? { activated } : {},
skip: (page - 1) * limit,
take: limit,
order: { createdDate: 'DESC' },
});
return { admins, total };
}

async findAdmin(adminId: number) {
Expand Down
35 changes: 20 additions & 15 deletions src/modules/admin/admin.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ import { CreateItemReqDto } from './dto/request/createItemReq.dto';
import { Item } from '../../entities/item.entity';
import { ItemResDto } from '../home/dto/response/itemRes.dto';
import { ThemeResDto } from '../home/dto/response/themeRes.dto';
import { UserResDto } from '../user/dto/response/userRes.dto';
import { EssayService } from '../essay/essay.service';

@Injectable()
export class AdminService {
Expand All @@ -80,6 +82,7 @@ export class AdminService {
private readonly alertService: AlertService,
private readonly geulroquisService: GeulroquisService,
private readonly geulroquisRepository: GeulroquisRepository,
private readonly essayService: EssayService,
private readonly nicknameService: NicknameService,
private readonly mailService: MailService,
private readonly awsService: AwsService,
Expand Down Expand Up @@ -506,10 +509,17 @@ export class AdminService {
return { users: userDtos, totalPage, page, total };
}

async searchUser(email: string) {
const user = await this.userRepository.findUserByEmail(email);
async searchUsers(keyword: string, page: number, limit: number) {
const { users, total } = await this.userRepository.searchUsers(keyword, page, limit);

return this.utilsService.transformToDto(UserDetailResDto, user);
const totalPage: number = Math.ceil(total / limit);

const usersDto = this.utilsService.transformToDto(UserResDto, users);
return { usersDto, total, page, totalPage };
}
Comment on lines +512 to +519
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix inconsistent property naming in return object.

The method returns usersDto but the pattern across other methods suggests it should be users.

Apply this fix:

-    return { usersDto, total, page, totalPage };
+    return { users: usersDto, total, page, totalPage };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async searchUsers(keyword: string, page: number, limit: number) {
const { users, total } = await this.userRepository.searchUsers(keyword, page, limit);
return this.utilsService.transformToDto(UserDetailResDto, user);
const totalPage: number = Math.ceil(total / limit);
const usersDto = this.utilsService.transformToDto(UserResDto, users);
return { usersDto, total, page, totalPage };
}
async searchUsers(keyword: string, page: number, limit: number) {
const { users, total } = await this.userRepository.searchUsers(keyword, page, limit);
const totalPage: number = Math.ceil(total / limit);
const usersDto = this.utilsService.transformToDto(UserResDto, users);
return { users: usersDto, total, page, totalPage };
}


async searchEssays(keyword: string, page: number, limit: number) {
return await this.essayService.searchEssays(keyword, page, limit);
Comment on lines +512 to +522
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add input validation for pagination parameters.

The pagination parameters (page and limit) should be validated to prevent potential DoS attacks through large page sizes or negative values.

Add validation at the start of both methods:

  async searchUsers(keyword: string, page: number, limit: number) {
+    if (page < 1 || limit < 1 || limit > 100) {
+      throw new HttpException('Invalid pagination parameters', HttpStatus.BAD_REQUEST);
+    }
    const { users, total } = await this.userRepository.searchUsers(keyword, page, limit);
    // ...
  }

  async searchEssays(keyword: string, page: number, limit: number) {
+    if (page < 1 || limit < 1 || limit > 100) {
+      throw new HttpException('Invalid pagination parameters', HttpStatus.BAD_REQUEST);
+    }
    return await this.essayService.searchEssays(keyword, page, limit);
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async searchUsers(keyword: string, page: number, limit: number) {
const { users, total } = await this.userRepository.searchUsers(keyword, page, limit);
return this.utilsService.transformToDto(UserDetailResDto, user);
const totalPage: number = Math.ceil(total / limit);
const usersDto = this.utilsService.transformToDto(UserResDto, users);
return { usersDto, total, page, totalPage };
}
async searchEssays(keyword: string, page: number, limit: number) {
return await this.essayService.searchEssays(keyword, page, limit);
async searchUsers(keyword: string, page: number, limit: number) {
if (page < 1 || limit < 1 || limit > 100) {
throw new HttpException('Invalid pagination parameters', HttpStatus.BAD_REQUEST);
}
const { users, total } = await this.userRepository.searchUsers(keyword, page, limit);
const totalPage: number = Math.ceil(total / limit);
const usersDto = this.utilsService.transformToDto(UserResDto, users);
return { usersDto, total, page, totalPage };
}
async searchEssays(keyword: string, page: number, limit: number) {
if (page < 1 || limit < 1 || limit > 100) {
throw new HttpException('Invalid pagination parameters', HttpStatus.BAD_REQUEST);
}
return await this.essayService.searchEssays(keyword, page, limit);

}

async getUser(userId: number) {
Expand Down Expand Up @@ -552,15 +562,7 @@ export class AdminService {
const { essays, total } = await this.essayRepository.findFullEssays(page, limit);
const totalPage: number = Math.ceil(total / limit);

const data = essays.map((essay) => ({
...essay,
authorId: essay.author.id,
storyId: essay.story?.id ?? null,
reportCount: essay?.reports ? essay.reports.length : null,
reviewCount: essay?.createdDate ? essay.reviews.length : null,
}));

const essaysDto = this.utilsService.transformToDto(EssayInfoResDto, data);
const essaysDto = this.utilsService.transformToDto(EssayInfoResDto, essays);
return { essays: essaysDto, total, page, totalPage };
}
Comment on lines +565 to 567
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add input validation for pagination parameters in getFullEssays.

Consistent with other paginated methods, add validation for pagination parameters.

Add validation:

  async getFullEssays(page: number, limit: number) {
+    if (page < 1 || limit < 1 || limit > 100) {
+      throw new HttpException('Invalid pagination parameters', HttpStatus.BAD_REQUEST);
+    }
    const { essays, total } = await this.essayRepository.findFullEssays(page, limit);
    // ...
  }

Committable suggestion was skipped due to low confidence.


Expand Down Expand Up @@ -649,11 +651,14 @@ export class AdminService {
return !admin ? null : admin;
}

async getAdmins(activated?: boolean) {
const admins = await this.adminRepository.findAdmins(activated);
async getAdmins(page: number, limit: number, activated?: boolean) {
const { admins, total } = await this.adminRepository.findAdmins(activated, page, limit);

const totalPage: number = Math.ceil(total / limit);

const adminsDto = this.utilsService.transformToDto(AdminResDto, admins);

return { admins: adminsDto };
return { admins: adminsDto, total, page, totalPage };
Comment on lines +654 to +661
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add input validation for pagination parameters in getAdmins.

Similar to the search methods, pagination parameters should be validated.

Add validation:

  async getAdmins(page: number, limit: number, activated?: boolean) {
+    if (page < 1 || limit < 1 || limit > 100) {
+      throw new HttpException('Invalid pagination parameters', HttpStatus.BAD_REQUEST);
+    }
    const { admins, total } = await this.adminRepository.findAdmins(activated, page, limit);
    // ...
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async getAdmins(page: number, limit: number, activated?: boolean) {
const { admins, total } = await this.adminRepository.findAdmins(activated, page, limit);
const totalPage: number = Math.ceil(total / limit);
const adminsDto = this.utilsService.transformToDto(AdminResDto, admins);
return { admins: adminsDto };
return { admins: adminsDto, total, page, totalPage };
async getAdmins(page: number, limit: number, activated?: boolean) {
if (page < 1 || limit < 1 || limit > 100) {
throw new HttpException('Invalid pagination parameters', HttpStatus.BAD_REQUEST);
}
const { admins, total } = await this.adminRepository.findAdmins(activated, page, limit);
const totalPage: number = Math.ceil(total / limit);
const adminsDto = this.utilsService.transformToDto(AdminResDto, admins);
return { admins: adminsDto, total, page, totalPage };

}

async getAdmin(adminId: number) {
Expand Down
18 changes: 18 additions & 0 deletions src/modules/admin/dto/response/adminsRes.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
import { ApiProperty } from '@nestjs/swagger';
import { AdminResDto } from './adminRes.dto';
import { Expose } from 'class-transformer';
import { IsNumber } from 'class-validator';

export class AdminsResDto {
@ApiProperty({ type: [AdminResDto] })
@Expose()
admins: AdminResDto[];

@ApiProperty()
@IsNumber()
@Expose()
page: number;

@ApiProperty()
@IsNumber()
@Expose()
total: number;

@ApiProperty()
@IsNumber()
@Expose()
totalPage: number;
Comment on lines +11 to +24
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding min/max validation constraints

While the number validation is good, consider adding more specific constraints:

  • page should be >= 1
  • total should be >= 0
  • totalPage should be >= 0

Here's how you can enhance the validation:

  @ApiProperty()
- @IsNumber()
+ @IsNumber()
+ @Min(1)
  @Expose()
  page: number;

  @ApiProperty()
- @IsNumber()
+ @IsNumber()
+ @Min(0)
  @Expose()
  total: number;

  @ApiProperty()
- @IsNumber()
+ @IsNumber()
+ @Min(0)
  @Expose()
  totalPage: number;

Don't forget to add the import:

import { IsNumber, Min } from 'class-validator';

}
Loading
Loading