Project/AutoSchedule

5일차 - Team / TeamMember API 기능 구현

sowon02 2025. 11. 5. 11:00

오늘은 팀(Team) 및 팀 구성원(TeamMember) 관련 기능을 구현했다.

팀 생성, 초대, 조회 기능까지 백엔드 API로 완성했으며,

기능 테스트를 위해 프론트엔드에도 간단한  테스트 버튼을 추가했다.


- 백엔드

1) 팀 생성

# TeamContoroller.java
@PostMapping
    public ResponseEntity<TeamResponse> createTeam(@Valid @RequestBody TeamCreateRequest request) {
        TeamResponse response = teamService.createTeam(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }
    
# TeamService.java
@Transactional
    public TeamResponse createTeam(TeamCreateRequest request) {
        if (!StringUtils.hasText(request.getName())) {
            throw new IllegalArgumentException("팀 이름은 필수입니다.");
        }

        Team team = new Team();
        team.setName(request.getName().trim());
        team.setCreatedAt(OffsetDateTime.now());
        Team saved = teamRepository.save(team);
        return toResponse(saved);
    }

# TeamRepository.java
public interface TeamRepository extends JpaRepository<Team, Long> {
}

 

2) 팀 구성원 초대

# TeamController.java
@PostMapping("/{id}/invite")
    public ResponseEntity<TeamMemberResponse> inviteMember(@PathVariable Long id, @Valid @RequestBody TeamInviteRequest request) {
        return ResponseEntity.status(HttpStatus.CREATED).body(teamService.inviteMember(id, request));
    }
    
@ TeamService.java
@Transactional
public TeamMemberResponse inviteMember(Long teamId, TeamInviteRequest request) {
    // 1. 팀 조회
    Team team = teamRepository.findById(teamId)
        .orElseThrow(() -> new IllegalArgumentException("팀을 찾을 수 없습니다: " + teamId));

    // 2. 사용자 조회 (userId 또는 email로)
    User user = findUser(request);
    
    // 3. 중복 체크
    TeamMemberId id = new TeamMemberId(team.getId(), user.getId());
    if (teamMemberRepository.existsById(id)) {
        throw new IllegalStateException("이미 팀에 속해 있습니다.");
    }

    // 4. 팀 구성원 생성 및 저장
    TeamMember member = new TeamMember();
    member.setId(id);
    member.setTeam(team);
    member.setUser(user);
    member.setRole(request.getRole() != null ? request.getRole() : Role.MEMBER);
    teamMemberRepository.save(member);

    return toMemberResponse(member);
}

// 내부 메서드: 사용자 찾기
private User findUser(TeamInviteRequest request) {
    if (request.getUserId() != null) {
        return userRepository.findById(request.getUserId())
            .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다: " + request.getUserId()));
    } else if (StringUtils.hasText(request.getEmail())) {
        return userRepository.findByEmail(request.getEmail())
            .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다: " + request.getEmail()));
    } else {
        throw new IllegalArgumentException("userId 또는 email 중 하나는 필수입니다.");
    }
}

# TeamRepository.java
public interface TeamMemberRepository extends JpaRepository<TeamMember, TeamMemberId> {
    List<TeamMember> findByTeam_Id(Long teamId);
    List<TeamMember> findByUser_Id(Long userId);
}

 

3) 팀 조회

# TeamContorller.java
// 사용자가 속한 팀 목록 조회
@GetMapping("/user/{userId}")
public ResponseEntity<List<TeamResponse>> getTeamsByUser(@PathVariable Long userId) {
    return ResponseEntity.ok(teamService.findTeamsByUser(userId));
}

// 특정 팀 조회
@GetMapping("/{id}")
public ResponseEntity<TeamResponse> getTeam(@PathVariable Long id) {
    return ResponseEntity.ok(teamService.findById(id));
}

// 모든 팀 조회
@GetMapping
public ResponseEntity<List<TeamResponse>> getAllTeams() {
    return ResponseEntity.ok(teamService.findAll());
}

# TeamService.java
// 사용자가 속한 팀 목록 조회
@Transactional(readOnly = true)
public List<TeamResponse> findTeamsByUser(Long userId) {
    return teamMemberRepository.findByUser_Id(userId).stream()
        .map(TeamMember::getTeam)
        .map(this::toResponse)
        .collect(Collectors.toList());
}


// 특정 팀 조회
@Transactional(readOnly = true)
public TeamResponse findById(Long id) {
    Team team = teamRepository.findById(id)
        .orElseThrow(() -> new IllegalArgumentException("팀을 찾을 수 없습니다: " + id));
    return toResponse(team);
}

// 모든 팀 조회 
@Transactional(readOnly = true)
public List<TeamResponse> findAll() {
    return teamRepository.findAll().stream()
        .map(this::toResponse)
        .collect(Collectors.toList());
}

 

4) 팀 구성원 목록 조회 

# TeamController.java
@GetMapping("/{id}/members")
public ResponseEntity<List<TeamMemberResponse>> getMembers(@PathVariable Long id) {
    return ResponseEntity.ok(teamService.listMembers(id));
}

# TeamService.java
@Transactional(readOnly = true)
public List<TeamMemberResponse> listMembers(Long teamId) {
    return teamMemberRepository.findByTeam_Id(teamId).stream()
        .map(this::toMemberResponse)
        .collect(Collectors.toList());
}

 

 

- 프론트엔드

메인화면에는 로그인된 유저의 스케쥴을 캘린더로 확인할 수 있도록하며, 상단에 팀 설정을 할 수 있도록 하였다.

팀 설정은 드롭다운을 사용하여

 

1. 유저가 속한 팀 확인

useEffect(() => {
    const load = async () => {
      if (!user?.id) return
      const { data } = await api.get(`/api/teams/user/${user.id}`)
      setTeams(data)
    }
    load()
  }, [user?.id])

 

2. 새로운 팀 추가

  <div className="border-t mt-1 pt-1 px-1">
                <button
                  className="w-full px-3 py-2 rounded bg-blue-600 text-white text-sm hover:bg-blue-700"
                  onClick={async () => {
                    const name = window.prompt('새 팀 이름을 입력하세요')
                    if (!name || !name.trim()) return
                    const { data } = await api.post('/api/teams', { name: name.trim() })
                    try { await api.post(`/api/teams/${data.id}/invite`, { userId: user?.id, role: 'OWNER' }) } catch {}
                    setOpen(false)
                    navigate(`/team/${data.id}/settings`)
                  }}
                >
                  + 팀 추가
                </button>

팀 추가 버튼 - 새로운 팀 이름 초기화

팀 추가 버튼을 누른 후에는 팀 이름을 정하는 window.prompt가 열린다

→ 이름을 입력한 뒤 확인을 누르면 

→ 'team/{team_id}/settings'로 넘어간다

여기서는 팀 목록 표시, 팀 멤버 목록, 팀 멤버 초대가 가능하다. 


프론트 스타일 관련 참고

  • 현재 frontend/src/index.css에서 TailwindCSS가 빌드되지 않아
    임시로 인라인 스타일을 적용한 상태
  • 캘린더 CSS는 CDN으로 로드되어 정상 표시됨
  • 추후 아래 설정으로 Tailwind 재활성화 예정: 
  •  
    @tailwind components;
    @tailwind utilities;
    tailwind.config.js / postcss.config.js 세팅 필요
  • @tailwind base;