Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ public class Member extends BaseTimeEntity {
@Column(name = "welcome_mail_sent_at")
private Instant welcomeMailSentAt;

// 홈페이지 "함께 할 멤버들"에만 쓴다(T-18). 쓰기 경로를 만들지 않는다(INV-13) —
// 명부 관리 책임은 auth/member 담당자에게 그대로 있다.
@Column(name = "profile_image_url")
private String profileImageUrl;

@Builder
private Member(String studentNumber, String password, String name, TrackMaster track, String generation,
MemberType memberType, String university, String email, String phoneNumber, String githubId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.bcsdlab.bcsdinternalapiv2.track.controller;

import com.bcsdlab.bcsdinternalapiv2.global.controller.dto.request.OrderRequest;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.request.MemberVisibilityRequest;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.request.TrackPageMembersAttachRequest;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.response.AdminTrackPageMemberResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import org.springframework.http.ResponseEntity;

@Tag(name = "관리자 트랙 멤버 API")
public interface AdminTrackPageMemberApi {

@ApiResponses(value = {
@ApiResponse(responseCode = "200"),
@ApiResponse(responseCode = "404", description = "존재하지 않는 트랙 페이지"),
})
@Operation(summary = "트랙에 배정된 부원 목록", description = "명부 join(이름·등급·프로필 사진). 부원 검색은 GET /members?name=를 그대로 쓴다.")
List<AdminTrackPageMemberResponse> getMembers(Long trackPageId);

@ApiResponses(value = {
@ApiResponse(responseCode = "200"),
@ApiResponse(responseCode = "404", description = "존재하지 않는 트랙 페이지 또는 부원"),
@ApiResponse(responseCode = "409", description = "이미 배정된 부원(INV-8)"),
})
@Operation(summary = "부원 배정")
List<AdminTrackPageMemberResponse> attachMembers(Long trackPageId,
@Valid TrackPageMembersAttachRequest request);

@ApiResponses(value = {
@ApiResponse(responseCode = "204"),
@ApiResponse(responseCode = "404", description = "배정되지 않은 부원"),
})
@Operation(summary = "배정 해제")
ResponseEntity<Void> detachMember(Long trackPageId, Long memberId);

@ApiResponses(value = {
@ApiResponse(responseCode = "204"),
@ApiResponse(responseCode = "404", description = "배정되지 않은 부원"),
})
@Operation(summary = "숨김/공개 전환")
ResponseEntity<Void> updateVisibility(Long trackPageId, Long memberId,
@Valid MemberVisibilityRequest request);

@ApiResponses(value = {
@ApiResponse(responseCode = "204"),
@ApiResponse(responseCode = "400", description = "id 집합 불일치(INV-4)"),
})
@Operation(summary = "순서 변경")
ResponseEntity<Void> reorder(Long trackPageId, @Valid OrderRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.bcsdlab.bcsdinternalapiv2.track.controller;

import com.bcsdlab.bcsdinternalapiv2.global.controller.dto.request.OrderRequest;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.request.MemberVisibilityRequest;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.request.TrackPageMembersAttachRequest;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.response.AdminTrackPageMemberResponse;
import com.bcsdlab.bcsdinternalapiv2.track.service.AdminTrackPageMemberService;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/v1/admin/track-pages/{trackPageId}/members")
@RequiredArgsConstructor
public class AdminTrackPageMemberController implements AdminTrackPageMemberApi {

private final AdminTrackPageMemberService adminTrackPageMemberService;

@Override
@GetMapping
public List<AdminTrackPageMemberResponse> getMembers(@PathVariable Long trackPageId) {
return adminTrackPageMemberService.getMembers(trackPageId);
}

@Override
@PostMapping
public List<AdminTrackPageMemberResponse> attachMembers(@PathVariable Long trackPageId,
@Valid @RequestBody TrackPageMembersAttachRequest request) {
return adminTrackPageMemberService.attachMembers(trackPageId, request);
}

@Override
@DeleteMapping("/{memberId}")
public ResponseEntity<Void> detachMember(@PathVariable Long trackPageId, @PathVariable Long memberId) {
adminTrackPageMemberService.detachMember(trackPageId, memberId);
return ResponseEntity.noContent().build();
}

@Override
@PatchMapping("/{memberId}/visibility")
public ResponseEntity<Void> updateVisibility(@PathVariable Long trackPageId, @PathVariable Long memberId,
@Valid @RequestBody MemberVisibilityRequest request) {
adminTrackPageMemberService.updateVisibility(trackPageId, memberId, request);
return ResponseEntity.noContent().build();
}

@Override
@PatchMapping("/order")
public ResponseEntity<Void> reorder(@PathVariable Long trackPageId, @Valid @RequestBody OrderRequest request) {
adminTrackPageMemberService.reorder(trackPageId, request);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.bcsdlab.bcsdinternalapiv2.track.controller.dto.request;

public record MemberVisibilityRequest(boolean isVisible) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.bcsdlab.bcsdinternalapiv2.track.controller.dto.request;

import jakarta.validation.constraints.NotEmpty;
import java.util.List;

public record TrackPageMembersAttachRequest(
@NotEmpty List<Long> memberIds
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.bcsdlab.bcsdinternalapiv2.track.controller.dto.response;

import com.bcsdlab.bcsdinternalapiv2.member.model.Member;
import com.bcsdlab.bcsdinternalapiv2.track.model.TrackPageMember;

/**
* 편집 화면(관리자)이 쓰는 배정 목록. 저트래픽 편집 화면이라 부원 조회의 N+1을
* 감수한다(T-11 관리자 커리큘럼 트리와 같은 이유).
*/
public record AdminTrackPageMemberResponse(
Long id,
Long memberId,
String name,
String memberType,
String profileImageUrl,
boolean isVisible,
int displayOrder
) {
public static AdminTrackPageMemberResponse from(TrackPageMember trackPageMember) {
Member member = trackPageMember.getMember();
return new AdminTrackPageMemberResponse(
trackPageMember.getId(),
member.getId(),
member.getName(),
member.getMemberType().name(),
member.getProfileImageUrl(),
trackPageMember.isVisible(),
trackPageMember.getDisplayOrder()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
import com.bcsdlab.bcsdinternalapiv2.track.model.TrackPage;
import java.util.List;

/**
* members(T-18)는 T-18이 이 레코드에 필드를 추가하는 방식으로 확장한다 — 지금 존재하지
* 않는 데이터를 위해 빈 자리표시자 구조를 미리 만들지 않는다.
*/
public record TrackDetailResponse(
String slug,
String name,
Expand All @@ -17,10 +13,12 @@ public record TrackDetailResponse(
String seoDescription,
List<StudyPointResponse> studyPoints,
List<TechStackSummaryResponse> techStacks,
CurriculumResponse curriculum
CurriculumResponse curriculum,
List<TrackMemberResponse> members
) {
public static TrackDetailResponse of(TrackPage trackPage, List<StudyPointResponse> studyPoints,
List<TechStackSummaryResponse> techStacks, CurriculumResponse curriculum) {
List<TechStackSummaryResponse> techStacks, CurriculumResponse curriculum,
List<TrackMemberResponse> members) {
return new TrackDetailResponse(
trackPage.getSlug(),
trackPage.getDisplayName(),
Expand All @@ -30,7 +28,8 @@ public static TrackDetailResponse of(TrackPage trackPage, List<StudyPointRespons
trackPage.getSeoDescription(),
studyPoints,
techStacks,
curriculum
curriculum,
members
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.bcsdlab.bcsdinternalapiv2.track.controller.dto.response;

import com.bcsdlab.bcsdinternalapiv2.member.model.Member;

public record TrackMemberResponse(
String name,
String memberType,
String profileImageUrl
) {
public static TrackMemberResponse from(Member member) {
return new TrackMemberResponse(member.getName(), member.getMemberType().name(), member.getProfileImageUrl());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ public enum TrackExceptionType implements BcsdExceptionType {
TRACK_PAGE_ALREADY_EXISTS(HttpStatus.CONFLICT, "이미 홈페이지 프로필이 있는 트랙입니다."),
TECH_STACK_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 기술스택입니다."),
TECH_STACK_NAME_DUPLICATED(HttpStatus.CONFLICT, "이미 등록된 기술스택입니다."),
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 부원입니다."),
MEMBER_ALREADY_ASSIGNED(HttpStatus.CONFLICT, "이미 이 트랙에 배정된 부원입니다."),
TRACK_PAGE_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "이 트랙에 배정되지 않은 부원입니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.bcsdlab.bcsdinternalapiv2.track.model;

import com.bcsdlab.bcsdinternalapiv2.global.BaseTimeEntity;
import com.bcsdlab.bcsdinternalapiv2.member.model.Member;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

/**
* 트랙 페이지에 배정된 부원 한 명(ADR-012). {@code member}는 이미 구현된 명부를 그대로
* 참조한다 — 포트 추상화 없이 {@code MemberRepository}를 직접 조회한다. 이름·등급·프로필
* 사진에 대한 쓰기 경로는 만들지 않는다(INV-13). 하위 항목이라 soft delete를 쓰지 않고
* {@code track_page} 삭제 시 DB의 on delete cascade로 함께 지워진다.
*/
@Getter
@Entity
@Table(name = "track_page_member")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TrackPageMember extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "track_page_id", nullable = false, updatable = false)
private TrackPage trackPage;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false, updatable = false)
private Member member;

@Column(name = "display_order", nullable = false)
private int displayOrder;

@Column(name = "is_visible", nullable = false)
private boolean visible;

@Builder
private TrackPageMember(TrackPage trackPage, Member member, int displayOrder, boolean visible) {
this.trackPage = trackPage;
this.member = member;
this.displayOrder = displayOrder;
this.visible = visible;
}

public void updateVisible(boolean visible) {
this.visible = visible;
}

public void updateDisplayOrder(int displayOrder) {
this.displayOrder = displayOrder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.bcsdlab.bcsdinternalapiv2.track.repository;

import com.bcsdlab.bcsdinternalapiv2.track.model.TrackPageMember;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TrackPageMemberRepository extends JpaRepository<TrackPageMember, Long> {

List<TrackPageMember> findAllByTrackPage_IdOrderByDisplayOrderAsc(Long trackPageId);

List<TrackPageMember> findAllByTrackPage_IdAndVisibleTrueOrderByDisplayOrderAsc(Long trackPageId);

boolean existsByTrackPage_IdAndMember_Id(Long trackPageId, Long memberId);

Optional<TrackPageMember> findByTrackPage_IdAndMember_Id(Long trackPageId, Long memberId);
}
Loading