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,18 @@
package com.bcsdlab.bcsdinternalapiv2.activity.controller;

import com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.response.ActivityCategoryResponse;
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 java.util.List;

@Tag(name = "활동 API (홈페이지 공개)")
public interface ActivityCategoryApi {

@ApiResponses(value = {
@ApiResponse(responseCode = "200"),
})
@Operation(summary = "공개 활동 카테고리 목록", description = "탭 노출 순서(display_order)대로 공개된 카테고리만 반환합니다.")
List<ActivityCategoryResponse> getCategories();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.bcsdlab.bcsdinternalapiv2.activity.controller;

import com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.response.ActivityCategoryResponse;
import com.bcsdlab.bcsdinternalapiv2.activity.service.ActivityCategoryService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/v1/activity-categories")
@RequiredArgsConstructor
public class ActivityCategoryController implements ActivityCategoryApi {

private final ActivityCategoryService activityCategoryService;

@Override
@GetMapping
public List<ActivityCategoryResponse> getCategories() {
return activityCategoryService.getCategories();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.bcsdlab.bcsdinternalapiv2.activity.controller;

import com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.request.ActivityCategoryCreateRequest;
import com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.request.ActivityCategoryUpdateRequest;
import com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.response.AdminActivityCategoryResponse;
import com.bcsdlab.bcsdinternalapiv2.global.controller.dto.request.OrderRequest;
import com.bcsdlab.bcsdinternalapiv2.global.controller.dto.request.PublishRequest;
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.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;

@Tag(name = "관리자 - 활동 카테고리 API")
@SecurityRequirement(name = "JWT")
public interface AdminActivityCategoryApi {

@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 = "활동 카테고리 목록", description = "숨김 포함 전체, display_order 순.")
List<AdminActivityCategoryResponse> getCategories();

@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<AdminActivityCategoryResponse> createCategory(
@RequestBody @Valid ActivityCategoryCreateRequest request);

@ApiResponses(value = {
@ApiResponse(responseCode = "200"),
@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 = "slug는 불변이다.")
AdminActivityCategoryResponse updateCategory(@PathVariable Long id,
@RequestBody @Valid ActivityCategoryUpdateRequest request);

@ApiResponses(value = {
@ApiResponse(responseCode = "204"),
@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))),
@ApiResponse(responseCode = "409", content = @Content(schema = @Schema(hidden = true))),
})
@Operation(summary = "활동 카테고리 삭제", description = "soft delete. 활동이 남아 있으면 409다(AC-3.8).")
ResponseEntity<Void> deleteCategory(@PathVariable Long id);

@ApiResponses(value = {
@ApiResponse(responseCode = "204"),
@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 = "공개/숨김")
ResponseEntity<Void> publish(@PathVariable Long id, @RequestBody @Valid PublishRequest request);

@ApiResponses(value = {
@ApiResponse(responseCode = "204"),
@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))),
})
@Operation(summary = "탭 순서 변경")
ResponseEntity<Void> reorder(@RequestBody @Valid OrderRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.bcsdlab.bcsdinternalapiv2.activity.controller;

import com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.request.ActivityCategoryCreateRequest;
import com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.request.ActivityCategoryUpdateRequest;
import com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.response.AdminActivityCategoryResponse;
import com.bcsdlab.bcsdinternalapiv2.activity.service.AdminActivityCategoryService;
import com.bcsdlab.bcsdinternalapiv2.global.controller.dto.request.OrderRequest;
import com.bcsdlab.bcsdinternalapiv2.global.controller.dto.request.PublishRequest;
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.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.PutMapping;
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/activity-categories")
@RequiredArgsConstructor
public class AdminActivityCategoryController implements AdminActivityCategoryApi {

private final AdminActivityCategoryService adminActivityCategoryService;

@Override
@GetMapping
public List<AdminActivityCategoryResponse> getCategories() {
return adminActivityCategoryService.getCategories();
}

@Override
@PostMapping
public ResponseEntity<AdminActivityCategoryResponse> createCategory(
@Valid @RequestBody ActivityCategoryCreateRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(adminActivityCategoryService.createCategory(request));
}

@Override
@PutMapping("/{id}")
public AdminActivityCategoryResponse updateCategory(@PathVariable Long id,
@Valid @RequestBody ActivityCategoryUpdateRequest request) {
return adminActivityCategoryService.updateCategory(id, request);
}

@Override
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteCategory(@PathVariable Long id) {
adminActivityCategoryService.deleteCategory(id);
return ResponseEntity.noContent().build();
}

@Override
@PatchMapping("/{id}/publish")
public ResponseEntity<Void> publish(@PathVariable Long id, @Valid @RequestBody PublishRequest request) {
adminActivityCategoryService.publish(id, request);
return ResponseEntity.noContent().build();
}

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

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

public record ActivityCategoryCreateRequest(
@NotBlank
@Pattern(regexp = "^[a-z0-9]+(-[a-z0-9]+)*$", message = "slug는 소문자·숫자·하이픈만 사용할 수 있습니다.")
String slug,

@NotBlank
@Size(max = 30)
String name,

@Size(max = 200)
String headline,

String heroImageUrl
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record ActivityCategoryUpdateRequest(
@NotBlank
@Size(max = 30)
String name,

@Size(max = 200)
String headline,

String heroImageUrl
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.response;

import com.bcsdlab.bcsdinternalapiv2.activity.model.ActivityCategory;

public record ActivityCategoryResponse(
String slug,
String name,
String headline,
String heroImageUrl
) {
public static ActivityCategoryResponse from(ActivityCategory category) {
return new ActivityCategoryResponse(
category.getSlug(), category.getName(), category.getHeadline(), category.getHeroImageUrl());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.response;

import com.bcsdlab.bcsdinternalapiv2.activity.model.ActivityCategory;

/**
* 활동 건수(activity 잔존 여부 판단에 쓰는 그 수)는 아직 없다 — Activity가 생기는
* T-15에서 이 레코드에 필드를 추가한다.
*/
public record AdminActivityCategoryResponse(
Long id,
String slug,
String name,
String headline,
String heroImageUrl,
boolean isPublished,
int displayOrder
) {
public static AdminActivityCategoryResponse from(ActivityCategory category) {
return new AdminActivityCategoryResponse(
category.getId(), category.getSlug(), category.getName(), category.getHeadline(),
category.getHeroImageUrl(), category.isPublished(), category.getDisplayOrder());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
public enum ActivityExceptionType implements BcsdExceptionType {

ACTIVITY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 활동입니다."),
CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 활동 카테고리입니다."),
CATEGORY_SLUG_DUPLICATED(HttpStatus.CONFLICT, "이미 사용 중인 카테고리 주소입니다."),
;

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

import com.bcsdlab.bcsdinternalapiv2.global.SoftDeletableEntity;
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;
import org.hibernate.annotations.SQLRestriction;

/**
* 활동 탭(EVENT/GAME/KOIN 등). 최상위 엔티티라 soft delete를 쓴다(ADR-006).
* 삭제 시 "활동이 남아 있으면 거부"(AC-3.8)는 Activity 엔티티가 생기는 T-15에서
* 함께 구현한다 — 지금은 Activity가 없어 검증할 대상이 없다.
*/
@Getter
@Entity
@Table(name = "activity_category")
@SQLRestriction("deleted_at is null")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ActivityCategory extends SoftDeletableEntity {

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

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

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

@Column(name = "headline")
private String headline;

@Column(name = "hero_image_url")
private String heroImageUrl;

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

@Column(name = "is_published", nullable = false)
private boolean published;

@Builder
private ActivityCategory(String slug, String name, String headline, String heroImageUrl, int displayOrder,
boolean published) {
this.slug = slug;
this.name = name;
this.headline = headline;
this.heroImageUrl = heroImageUrl;
this.displayOrder = displayOrder;
this.published = published;
}

public void updateHeader(String name, String headline, String heroImageUrl) {
this.name = name;
this.headline = headline;
this.heroImageUrl = heroImageUrl;
}

public void updatePublished(boolean published) {
this.published = published;
}

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

import com.bcsdlab.bcsdinternalapiv2.activity.model.ActivityCategory;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ActivityCategoryRepository extends JpaRepository<ActivityCategory, Long> {

List<ActivityCategory> findAllByOrderByDisplayOrderAsc();

List<ActivityCategory> findAllByPublishedTrueOrderByDisplayOrderAsc();

boolean existsBySlug(String slug);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.bcsdlab.bcsdinternalapiv2.activity.service;

import com.bcsdlab.bcsdinternalapiv2.activity.controller.dto.response.ActivityCategoryResponse;
import com.bcsdlab.bcsdinternalapiv2.activity.repository.ActivityCategoryRepository;
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 ActivityCategoryService {

private final ActivityCategoryRepository activityCategoryRepository;

public List<ActivityCategoryResponse> getCategories() {
return activityCategoryRepository.findAllByPublishedTrueOrderByDisplayOrderAsc().stream()
.map(ActivityCategoryResponse::from)
.toList();
}
}
Loading