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
@@ -0,0 +1,37 @@
package com.bcsdlab.bcsdinternalapiv2.track.controller;

import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.request.TechStackCreateRequest;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.response.TechStackResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;

@Tag(name = "관리자 - 기술스택 마스터 API")
@SecurityRequirement(name = "JWT")
public interface AdminTechStackApi {

@ApiResponses(value = {
@ApiResponse(responseCode = "200"),
@ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))),
})
@Operation(summary = "기술스택 마스터 목록")
List<TechStackResponse> getTechStacks();

@ApiResponses(value = {
@ApiResponse(responseCode = "201"),
@ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "409", content = @Content(schema = @Schema(hidden = true))),
})
@Operation(summary = "기술스택 마스터 생성")
ResponseEntity<TechStackResponse> createTechStack(@RequestBody @Valid TechStackCreateRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.bcsdlab.bcsdinternalapiv2.track.controller;

import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.request.TechStackCreateRequest;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.response.TechStackResponse;
import com.bcsdlab.bcsdinternalapiv2.track.service.AdminTechStackService;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
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/tech-stacks")
@RequiredArgsConstructor
public class AdminTechStackController implements AdminTechStackApi {

private final AdminTechStackService adminTechStackService;

@Override
@GetMapping
public List<TechStackResponse> getTechStacks() {
return adminTechStackService.getTechStacks();
}

@Override
@PostMapping
public ResponseEntity<TechStackResponse> createTechStack(@Valid @RequestBody TechStackCreateRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(adminTechStackService.createTechStack(request));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.request.TrackPageCreateRequest;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.request.TrackPageUpdateRequest;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.request.StudyPointsReplaceRequest;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.request.TechStacksReplaceRequest;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.response.AdminTrackPageDetailResponse;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.response.AdminTrackPageSummaryResponse;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.response.StudyPointResponse;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.response.TechStackResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
Expand Down Expand Up @@ -111,4 +113,15 @@ AdminTrackPageDetailResponse updateTrackPage(@PathVariable Long id,
@Operation(summary = "WHAT WE STUDY 카드 전체 교체", description = "최대 4개(INV-14). 배열 순서가 display_order다.")
List<StudyPointResponse> replaceStudyPoints(@PathVariable Long id,
@RequestBody @Valid StudyPointsReplaceRequest request);

@ApiResponses(value = {
@ApiResponse(responseCode = "200"),
@ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))),
})
@Operation(summary = "기술스택 전체 교체", description = "배열 순서가 display_order다.")
List<TechStackResponse> replaceTechStacks(@PathVariable Long id,
@RequestBody @Valid TechStacksReplaceRequest request);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import com.bcsdlab.bcsdinternalapiv2.global.controller.dto.request.PublishRequest;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.request.SlugChangeRequest;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.request.StudyPointsReplaceRequest;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.request.TechStacksReplaceRequest;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.request.TrackPageCreateRequest;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.request.TrackPageUpdateRequest;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.response.AdminTrackPageDetailResponse;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.response.AdminTrackPageSummaryResponse;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.response.StudyPointResponse;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.response.TechStackResponse;
import com.bcsdlab.bcsdinternalapiv2.track.service.AdminTrackPageService;
import jakarta.validation.Valid;
import java.util.List;
Expand Down Expand Up @@ -92,4 +94,11 @@ public List<StudyPointResponse> replaceStudyPoints(@PathVariable Long id,
@Valid @RequestBody StudyPointsReplaceRequest request) {
return adminTrackPageService.replaceStudyPoints(id, request);
}

@Override
@PutMapping("/{id}/tech-stacks")
public List<TechStackResponse> replaceTechStacks(@PathVariable Long id,
@Valid @RequestBody TechStacksReplaceRequest request) {
return adminTrackPageService.replaceTechStacks(id, request);
}
}
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.NotBlank;

public record TechStackCreateRequest(
@NotBlank String name,
@NotBlank String iconUrl
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.bcsdlab.bcsdinternalapiv2.track.controller.dto.request;

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

/**
* 트랙 페이지에 부착할 기술스택 전체 교체. 배열 순서가 곧 display_order다.
*/
public record TechStacksReplaceRequest(
@NotNull List<Long> techStackIds
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.bcsdlab.bcsdinternalapiv2.track.controller.dto.response;

import com.bcsdlab.bcsdinternalapiv2.track.model.TechStack;

/**
* 관리자용 — id를 포함해 선택·해제 UI가 특정 항목을 가리킬 수 있게 한다.
* 공개 응답에는 {@link TechStackSummaryResponse}(id 없음)를 쓴다.
*/
public record TechStackResponse(
Long id,
String name,
String iconUrl
) {
public static TechStackResponse from(TechStack techStack) {
return new TechStackResponse(techStack.getId(), techStack.getName(), techStack.getIconUrl());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.bcsdlab.bcsdinternalapiv2.track.controller.dto.response;

import com.bcsdlab.bcsdinternalapiv2.track.model.TechStack;

public record TechStackSummaryResponse(
String name,
String iconUrl
) {
public static TechStackSummaryResponse from(TechStack techStack) {
return new TechStackSummaryResponse(techStack.getName(), techStack.getIconUrl());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
import java.util.List;

/**
* techStacks(T-09)·curriculum(T-12)·members(T-18)는 각 도메인 티켓이 이 레코드에
* 필드를 추가하는 방식으로 확장한다 — 지금 존재하지 않는 데이터를 위해 빈 자리표시자
* 구조를 미리 만들지 않는다.
* curriculum(T-12)·members(T-18)는 각 도메인 티켓이 이 레코드에 필드를 추가하는 방식으로
* 확장한다 — 지금 존재하지 않는 데이터를 위해 빈 자리표시자 구조를 미리 만들지 않는다.
*/
public record TrackDetailResponse(
String slug,
Expand All @@ -15,17 +14,20 @@ public record TrackDetailResponse(
String heroImageUrl,
String ogImageUrl,
String seoDescription,
List<StudyPointResponse> studyPoints
List<StudyPointResponse> studyPoints,
List<TechStackSummaryResponse> techStacks
) {
public static TrackDetailResponse of(TrackPage trackPage, List<StudyPointResponse> studyPoints) {
public static TrackDetailResponse of(TrackPage trackPage, List<StudyPointResponse> studyPoints,
List<TechStackSummaryResponse> techStacks) {
return new TrackDetailResponse(
trackPage.getSlug(),
trackPage.getDisplayName(),
trackPage.getTagline(),
trackPage.getHeroImageUrl(),
trackPage.getOgImageUrl(),
trackPage.getSeoDescription(),
studyPoints
studyPoints,
techStacks
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public enum TrackExceptionType implements BcsdExceptionType {
TRACK_PAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 트랙 페이지입니다."),
TRACK_PAGE_SLUG_DUPLICATED(HttpStatus.CONFLICT, "이미 사용 중인 주소입니다."),
TRACK_PAGE_ALREADY_EXISTS(HttpStatus.CONFLICT, "이미 홈페이지 프로필이 있는 트랙입니다."),
TECH_STACK_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 기술스택입니다."),
TECH_STACK_NAME_DUPLICATED(HttpStatus.CONFLICT, "이미 등록된 기술스택입니다."),
;

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

import com.bcsdlab.bcsdinternalapiv2.global.BaseTimeEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

/**
* 기술스택 마스터. 여러 트랙 페이지가 공유한다(React, Java 등 재사용).
*/
@Getter
@Entity
@Table(name = "tech_stack")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TechStack extends BaseTimeEntity {

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

@Column(name = "name", nullable = false)
private String name;

@Column(name = "icon_url", nullable = false)
private String iconUrl;

@Builder
private TechStack(String name, String iconUrl) {
this.name = name;
this.iconUrl = iconUrl;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.bcsdlab.bcsdinternalapiv2.track.model;

import jakarta.persistence.Column;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.MapsId;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

/**
* 트랙 페이지 ↔ 기술스택 다대다 조인 + 표시 순서. 전체 교체(T-09)로만 관리한다.
*/
@Getter
@Entity
@Table(name = "track_page_tech_stack")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TrackPageTechStack {

@EmbeddedId
private TrackPageTechStackId id;

@ManyToOne(fetch = FetchType.LAZY)
@MapsId("trackPageId")
@JoinColumn(name = "track_page_id")
private TrackPage trackPage;

@ManyToOne(fetch = FetchType.LAZY)
@MapsId("techStackId")
@JoinColumn(name = "tech_stack_id")
private TechStack techStack;

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

public TrackPageTechStack(TrackPage trackPage, TechStack techStack, int displayOrder) {
this.id = new TrackPageTechStackId(trackPage.getId(), techStack.getId());
this.trackPage = trackPage;
this.techStack = techStack;
this.displayOrder = displayOrder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.bcsdlab.bcsdinternalapiv2.track.model;

import jakarta.persistence.Embeddable;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

@Embeddable
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class TrackPageTechStackId implements Serializable {

private Long trackPageId;
private Long techStackId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.bcsdlab.bcsdinternalapiv2.track.repository;

import com.bcsdlab.bcsdinternalapiv2.track.model.TechStack;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TechStackRepository extends JpaRepository<TechStack, Long> {

boolean existsByName(String name);
}
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.TrackPageTechStack;
import com.bcsdlab.bcsdinternalapiv2.track.model.TrackPageTechStackId;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface TrackPageTechStackRepository extends JpaRepository<TrackPageTechStack, TrackPageTechStackId> {

@Query("select tpts from TrackPageTechStack tpts join fetch tpts.techStack "
+ "where tpts.trackPage.id = :trackPageId order by tpts.displayOrder asc")
List<TrackPageTechStack> findAllByTrackPageIdOrderByDisplayOrderAsc(@Param("trackPageId") Long trackPageId);

void deleteAllByTrackPage_Id(Long trackPageId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.bcsdlab.bcsdinternalapiv2.track.service;

import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.request.TechStackCreateRequest;
import com.bcsdlab.bcsdinternalapiv2.track.controller.dto.response.TechStackResponse;
import com.bcsdlab.bcsdinternalapiv2.track.exception.TrackException;
import com.bcsdlab.bcsdinternalapiv2.track.exception.TrackExceptionType;
import com.bcsdlab.bcsdinternalapiv2.track.model.TechStack;
import com.bcsdlab.bcsdinternalapiv2.track.repository.TechStackRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AdminTechStackService {

private final TechStackRepository techStackRepository;

public List<TechStackResponse> getTechStacks() {
return techStackRepository.findAll().stream()
.map(TechStackResponse::from)
.toList();
}

@Transactional
public TechStackResponse createTechStack(TechStackCreateRequest request) {
if (techStackRepository.existsByName(request.name())) {
throw new TrackException(TrackExceptionType.TECH_STACK_NAME_DUPLICATED);
}

TechStack techStack = techStackRepository.save(
TechStack.builder().name(request.name()).iconUrl(request.iconUrl()).build());
return TechStackResponse.from(techStack);
}
}
Loading