Skip to content
Draft
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
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,8 @@ src/main/generated/
/.cursor/

### AntiGravity ###
/.agent/
/.agent/

### Claude ###
/.claude/
CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.dreamteam.alter.adapter.inbound.admin.posting.controller;

import com.dreamteam.alter.adapter.inbound.admin.posting.dto.AdminPostingDetailResponseDto;
import com.dreamteam.alter.adapter.inbound.admin.posting.dto.AdminPostingListFilterDto;
import com.dreamteam.alter.adapter.inbound.admin.posting.dto.AdminPostingListResponseDto;
import com.dreamteam.alter.adapter.inbound.admin.posting.dto.AdminUpdatePostingStatusRequestDto;
import com.dreamteam.alter.adapter.inbound.common.dto.CommonApiResponse;
import com.dreamteam.alter.adapter.inbound.common.dto.PageRequestDto;
import com.dreamteam.alter.adapter.inbound.common.dto.PaginatedResponseDto;
import com.dreamteam.alter.application.aop.AdminActionContext;
import com.dreamteam.alter.domain.posting.port.inbound.AdminDeletePostingUseCase;
import com.dreamteam.alter.domain.posting.port.inbound.AdminGetPostingDetailUseCase;
import com.dreamteam.alter.domain.posting.port.inbound.AdminGetPostingListUseCase;
import com.dreamteam.alter.domain.posting.port.inbound.AdminUpdatePostingStatusUseCase;
import com.dreamteam.alter.domain.user.context.AdminActor;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/admin/postings")
@PreAuthorize("hasAnyRole('ADMIN')")
@RequiredArgsConstructor
@Validated
public class AdminPostingController implements AdminPostingControllerSpec {

@Resource(name = "adminGetPostingList")
private final AdminGetPostingListUseCase adminGetPostingList;

@Resource(name = "adminGetPostingDetail")
private final AdminGetPostingDetailUseCase adminGetPostingDetail;

@Resource(name = "adminUpdatePostingStatus")
private final AdminUpdatePostingStatusUseCase adminUpdatePostingStatus;

@Resource(name = "adminDeletePosting")
private final AdminDeletePostingUseCase adminDeletePosting;

@Override
@GetMapping
public ResponseEntity<PaginatedResponseDto<AdminPostingListResponseDto>> getPostingList(
PageRequestDto request,
AdminPostingListFilterDto filter
) {
AdminActor actor = AdminActionContext.getInstance().getActor();

return ResponseEntity.ok(adminGetPostingList.execute(request, filter, actor));
}

@Override
@GetMapping("/{postingId}")
public ResponseEntity<CommonApiResponse<AdminPostingDetailResponseDto>> getPostingDetail(
@PathVariable Long postingId
) {
AdminActor actor = AdminActionContext.getInstance().getActor();

return ResponseEntity.ok(CommonApiResponse.of(adminGetPostingDetail.execute(postingId, actor)));
}

@Override
@PutMapping("/{postingId}/status")
public ResponseEntity<CommonApiResponse<Void>> updatePostingStatus(
@PathVariable Long postingId,
@Valid @RequestBody AdminUpdatePostingStatusRequestDto request
) {
AdminActor actor = AdminActionContext.getInstance().getActor();

adminUpdatePostingStatus.execute(postingId, request, actor);
return ResponseEntity.ok(CommonApiResponse.empty());
}

@Override
@DeleteMapping("/{postingId}")
public ResponseEntity<CommonApiResponse<Void>> deletePosting(
@PathVariable Long postingId
) {
AdminActor actor = AdminActionContext.getInstance().getActor();

adminDeletePosting.execute(postingId, actor);
return ResponseEntity.ok(CommonApiResponse.empty());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package com.dreamteam.alter.adapter.inbound.admin.posting.controller;

import com.dreamteam.alter.adapter.inbound.admin.posting.dto.AdminPostingDetailResponseDto;
import com.dreamteam.alter.adapter.inbound.admin.posting.dto.AdminPostingListFilterDto;
import com.dreamteam.alter.adapter.inbound.admin.posting.dto.AdminPostingListResponseDto;
import com.dreamteam.alter.adapter.inbound.admin.posting.dto.AdminUpdatePostingStatusRequestDto;
import com.dreamteam.alter.adapter.inbound.common.dto.CommonApiResponse;
import com.dreamteam.alter.adapter.inbound.common.dto.ErrorResponse;
import com.dreamteam.alter.adapter.inbound.common.dto.PageRequestDto;
import com.dreamteam.alter.adapter.inbound.common.dto.PaginatedResponseDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
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.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;

@Tag(name = "ADMIN - 관리자 공고 관리 API")
public interface AdminPostingControllerSpec {

@Operation(summary = "공고 목록 조회", description = "관리자가 공고 목록을 오프셋 페이징으로 조회합니다. 상태, 제목, 업장 ID로 필터링할 수 있습니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "공고 목록 조회 성공"),
@ApiResponse(responseCode = "400", description = "실패 케이스",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(
name = "잘못된 요청 (유효하지 않은 페이지, 페이지 크기 등)",
value = "{\"code\" : \"B001\"}"
)
}))
})
ResponseEntity<PaginatedResponseDto<AdminPostingListResponseDto>> getPostingList(
PageRequestDto request,
AdminPostingListFilterDto filter
);

@Operation(summary = "공고 상세 조회", description = "관리자가 공고 상세 정보를 조회합니다. 업장, 스케줄, 키워드 정보가 포함됩니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "공고 상세 조회 성공"),
@ApiResponse(responseCode = "400", description = "실패 케이스",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(
name = "존재하지 않는 공고",
value = "{\"code\" : \"B007\"}"
)
}))
})
ResponseEntity<CommonApiResponse<AdminPostingDetailResponseDto>> getPostingDetail(
@Parameter(description = "공고 ID", example = "1") @PathVariable Long postingId
);

@Operation(summary = "공고 상태 변경", description = "관리자가 공고 상태를 변경합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "공고 상태 변경 성공"),
@ApiResponse(responseCode = "400", description = "실패 케이스",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(
name = "존재하지 않는 공고",
value = "{\"code\" : \"B007\"}"
),
@ExampleObject(
name = "잘못된 요청 (유효하지 않은 상태 값 등)",
value = "{\"code\" : \"B001\"}"
)
})),
@ApiResponse(responseCode = "409", description = "상태 충돌",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(
name = "현재 상태와 동일한 상태로 변경 시도",
value = "{\"code\" : \"B020\"}"
)
}))
})
ResponseEntity<CommonApiResponse<Void>> updatePostingStatus(
@Parameter(description = "공고 ID", example = "1") @PathVariable Long postingId,
@Valid @RequestBody AdminUpdatePostingStatusRequestDto request
);

@Operation(summary = "공고 삭제", description = "관리자가 공고를 삭제합니다. (소프트 삭제)")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "공고 삭제 성공"),
@ApiResponse(responseCode = "400", description = "실패 케이스",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(
name = "존재하지 않는 공고",
value = "{\"code\" : \"B007\"}"
)
})),
@ApiResponse(responseCode = "409", description = "상태 충돌",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(
name = "이미 삭제된 공고",
value = "{\"code\" : \"B020\"}"
)
}))
})
ResponseEntity<CommonApiResponse<Void>> deletePosting(
@Parameter(description = "공고 ID", example = "1") @PathVariable Long postingId
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.dreamteam.alter.adapter.inbound.admin.posting.dto;

import com.dreamteam.alter.adapter.inbound.common.dto.DescribedEnumDto;
import com.dreamteam.alter.adapter.outbound.posting.persistence.readonly.AdminPostingDetailResponse;
import com.dreamteam.alter.domain.posting.type.PaymentType;
import com.dreamteam.alter.domain.posting.type.PostingStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.util.List;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder(access = AccessLevel.PRIVATE)
@Schema(description = "공고 상세 응답 DTO")
public class AdminPostingDetailResponseDto {

@Schema(description = "공고 ID", example = "1")
private Long id;

@Schema(description = "공고 제목", example = "주말 알바 모집")
private String title;

@Schema(description = "공고 설명")
private String description;

@Schema(description = "급여 금액", example = "10000")
private int payAmount;

@Schema(description = "급여 유형")
private DescribedEnumDto<PaymentType> paymentType;

@Schema(description = "공고 상태")
private DescribedEnumDto<PostingStatus> status;

@Schema(description = "생성일시", example = "2025-01-01T12:00:00")
private LocalDateTime createdAt;

@Schema(description = "수정일시", example = "2025-01-01T12:00:00")
private LocalDateTime updatedAt;

@Schema(description = "업장 정보")
private AdminPostingWorkspaceDto workspace;

@Schema(description = "스케줄 목록")
private List<AdminPostingScheduleDto> schedules;

@Schema(description = "키워드 목록")
private List<AdminPostingKeywordDto> keywords;

public static AdminPostingDetailResponseDto from(AdminPostingDetailResponse response) {
return AdminPostingDetailResponseDto.builder()
.id(response.getId())
.title(response.getTitle())
.description(response.getDescription())
.payAmount(response.getPayAmount())
.paymentType(DescribedEnumDto.of(response.getPaymentType(), PaymentType.describe()))
.status(DescribedEnumDto.of(response.getStatus(), PostingStatus.describe()))
.createdAt(response.getCreatedAt())
.updatedAt(response.getUpdatedAt())
.workspace(AdminPostingWorkspaceDto.from(response.getWorkspace()))
.schedules(response.getSchedules().stream()
.map(AdminPostingScheduleDto::from)
.toList())
.keywords(response.getKeywords().stream()
.map(AdminPostingKeywordDto::from)
.toList())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.dreamteam.alter.adapter.inbound.admin.posting.dto;

import com.dreamteam.alter.domain.posting.entity.PostingKeyword;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder(access = AccessLevel.PRIVATE)
@Schema(description = "공고 상세 - 키워드 정보 DTO")
public class AdminPostingKeywordDto {

@Schema(description = "키워드 ID", example = "1")
private Long id;

@Schema(description = "키워드명", example = "카페")
private String name;

public static AdminPostingKeywordDto from(PostingKeyword keyword) {
return AdminPostingKeywordDto.builder()
.id(keyword.getId())
.name(keyword.getName())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.dreamteam.alter.adapter.inbound.admin.posting.dto;

import com.dreamteam.alter.domain.posting.type.PostingStatus;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springdoc.core.annotations.ParameterObject;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ParameterObject
@Schema(description = "공고 목록 필터 DTO")
public class AdminPostingListFilterDto {

@Parameter(description = "공고 상태")
private PostingStatus status;

@Parameter(description = "공고 제목 검색어")
private String title;

@Parameter(description = "업장 ID")
private Long workspaceId;
}
Loading