feat: 멤버 관리 연동 + 사용자 검색 API (3단계)
- 멤버 목록/초대/역할변경/제거 프론트 연동(RepoMembersPage), 초대 자동완성 - GET /users 사용자 검색 API + UserController - RepoMembersPage 역할 변경 드롭다운 잘림(overflow) 수정 - InviteMember/UpdateMember DTO, RepoMember 엔티티 정비 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
IsEmail,
|
||||
IsIn,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
import { IsEmail, IsIn, IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import type { MemberRoleType } from '../entities/repo-member.entity';
|
||||
|
||||
@@ -26,14 +19,4 @@ export class InviteMemberDto {
|
||||
})
|
||||
@IsIn(['admin', 'member'], { message: '권한 역할을 선택해 주세요.' })
|
||||
roleType!: MemberRoleType;
|
||||
|
||||
@ApiProperty({
|
||||
description: '저장소 내 직무(선택)',
|
||||
example: '콘텐츠 디자인',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
subRole?: string;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { IsIn, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
import { IsIn, IsOptional } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import type { MemberRoleType } from '../entities/repo-member.entity';
|
||||
|
||||
// 멤버 수정 DTO — 권한 역할/저장소 내 직무 (owner 는 변경 불가)
|
||||
// 멤버 수정 DTO — 권한 역할 (owner 는 변경 불가)
|
||||
export class UpdateMemberDto {
|
||||
@ApiProperty({
|
||||
description: '권한 역할',
|
||||
@@ -12,10 +12,4 @@ export class UpdateMemberDto {
|
||||
@IsOptional()
|
||||
@IsIn(['admin', 'member'], { message: '권한 역할을 선택해 주세요.' })
|
||||
roleType?: MemberRoleType;
|
||||
|
||||
@ApiProperty({ description: '저장소 내 직무', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
subRole?: string;
|
||||
}
|
||||
|
||||
@@ -37,10 +37,6 @@ export class RepoMember {
|
||||
@Column({ name: 'is_owner', default: false })
|
||||
isOwner!: boolean;
|
||||
|
||||
// 저장소 내 직무 표기(예: '리드', '콘텐츠 디자인') — 선택
|
||||
@Column({ name: 'sub_role', type: 'varchar', nullable: true })
|
||||
subRole!: string | null;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
}
|
||||
|
||||
@@ -26,14 +26,11 @@ export class MemberController {
|
||||
constructor(private readonly memberService: MemberService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '저장소 멤버 목록 (멤버 또는 공개)' })
|
||||
@ApiOperation({ summary: '저장소 멤버 목록 (인증 사용자 열람 가능)' })
|
||||
@ApiResponse({ status: 200, description: '목록 조회 성공' })
|
||||
@ApiResponse({ status: 403, description: '접근 권한 없음 (AUTH_003)' })
|
||||
list(
|
||||
@Param('repoId') repoId: string,
|
||||
@CurrentUser() user: PublicUser,
|
||||
): Promise<RepoMemberResponse[]> {
|
||||
return this.memberService.list(repoId, user.id);
|
||||
@ApiResponse({ status: 404, description: '저장소를 찾을 수 없음 (RES_001)' })
|
||||
list(@Param('repoId') repoId: string): Promise<RepoMemberResponse[]> {
|
||||
return this.memberService.list(repoId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { UserService, type PublicUser } from './user.service';
|
||||
|
||||
// 사용자 컨트롤러 — 멤버 초대 자동완성을 위한 검색/목록 제공 (인증 필요)
|
||||
@ApiTags('User')
|
||||
@Controller('users')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class UserController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: '사용자 검색/목록 (멤버 초대 자동완성)',
|
||||
description: 'q 가 있으면 이메일/이름 부분일치, 없으면 전체 목록(이름순).',
|
||||
})
|
||||
@ApiQuery({ name: 'q', required: false, description: '검색어(이메일/이름)' })
|
||||
@ApiResponse({ status: 200, description: '목록 조회 성공' })
|
||||
async list(@Query('q') q?: string): Promise<PublicUser[]> {
|
||||
const users = await this.userService.search(q);
|
||||
return users.map((u) => UserService.toPublic(u));
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UserService } from './user.service';
|
||||
import { UserController } from './user.controller';
|
||||
import { User } from './entities/user.entity';
|
||||
|
||||
// 사용자 데이터 계층 모듈 — UserService 를 다른 모듈(Auth 등)에 제공
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])],
|
||||
controllers: [UserController],
|
||||
providers: [UserService],
|
||||
exports: [UserService],
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ILike, Repository } from 'typeorm';
|
||||
import { User } from './entities/user.entity';
|
||||
|
||||
// 외부 노출용 사용자 형태 (passwordHash 등 민감정보 제외)
|
||||
@@ -29,6 +29,21 @@ export class UserService {
|
||||
return this.userRepository.findOne({ where: { email } });
|
||||
}
|
||||
|
||||
// 사용자 검색/목록 (멤버 초대 자동완성용) — 이메일/이름 부분일치(대소문자 무시).
|
||||
// 검색어가 없으면 전체 목록(이름순, limit 개)을 반환한다.
|
||||
search(query: string | undefined, limit = 50): Promise<User[]> {
|
||||
const q = (query ?? '').trim();
|
||||
if (!q) {
|
||||
return this.userRepository.find({ order: { name: 'ASC' }, take: limit });
|
||||
}
|
||||
const like = ILike(`%${q}%`);
|
||||
return this.userRepository.find({
|
||||
where: [{ email: like }, { name: like }],
|
||||
order: { name: 'ASC' },
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
// 로그인 검증용 — passwordHash 를 명시적으로 포함하여 조회
|
||||
findByEmailWithPassword(email: string): Promise<User | null> {
|
||||
return this.userRepository
|
||||
|
||||
Reference in New Issue
Block a user