오늘은 팀(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;
'Project > AutoSchedule' 카테고리의 다른 글
| 7일차 - Swagger or Postman API 문서 작성 (0) | 2025.11.09 |
|---|---|
| 6일차 - Task/CalendarEvent API 검증 & 캘린더 통합 (0) | 2025.11.07 |
| 4일차 - 프론트엔드 환경 세팅 & 구조 설계 (0) | 2025.11.03 |
| 3일차 - 로그인, 회원가입 (0) | 2025.11.03 |
| 2일차 - JPA 엔티티 구현 (0) | 2025.10.31 |