Skip to content
Closed
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,84 @@
package com.dreamteam.alter.adapter.inbound.admin.posting.controller;

import com.dreamteam.alter.adapter.inbound.admin.posting.dto.AdminPostingKeywordRequestDto;
import com.dreamteam.alter.adapter.inbound.admin.posting.dto.AdminPostingKeywordResponseDto;
import com.dreamteam.alter.adapter.inbound.common.dto.CommonApiResponse;
import com.dreamteam.alter.domain.posting.command.AdminCreatePostingKeywordCommand;
import com.dreamteam.alter.domain.posting.command.AdminUpdatePostingKeywordCommand;
import com.dreamteam.alter.domain.posting.entity.PostingKeyword;
import com.dreamteam.alter.domain.posting.port.inbound.AdminCreatePostingKeywordUseCase;
import com.dreamteam.alter.domain.posting.port.inbound.AdminDeletePostingKeywordUseCase;
import com.dreamteam.alter.domain.posting.port.inbound.AdminGetPostingKeywordListUseCase;
import com.dreamteam.alter.domain.posting.port.inbound.AdminUpdatePostingKeywordUseCase;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/admin/posting-keywords")
@PreAuthorize("hasAnyRole('ADMIN')")
@RequiredArgsConstructor
@Validated
public class AdminPostingKeywordController implements AdminPostingKeywordControllerSpec {

@Resource(name = "adminGetPostingKeywordList")
private final AdminGetPostingKeywordListUseCase adminGetPostingKeywordList;

@Resource(name = "adminCreatePostingKeyword")
private final AdminCreatePostingKeywordUseCase adminCreatePostingKeyword;

@Resource(name = "adminUpdatePostingKeyword")
private final AdminUpdatePostingKeywordUseCase adminUpdatePostingKeyword;

@Resource(name = "adminDeletePostingKeyword")
private final AdminDeletePostingKeywordUseCase adminDeletePostingKeyword;
Comment on lines +31 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

@Resource@RequiredArgsConstructor 중복은 불필요합니다.

@RequiredArgsConstructor로 생성자 주입이 이미 처리되며, 파라미터명이 빈 이름과 일치하므로 @Resource(name = "...")는 중복입니다. 제거를 고려해 보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/dreamteam/alter/adapter/inbound/admin/posting/controller/AdminPostingKeywordController.java`
around lines 30 - 40, `@RequiredArgsConstructor가` 생성자 주입을 처리하므로
AdminPostingKeywordController의 네 개 final 의존성 필드에서 중복된 `@Resource`(name = "...")
어노테이션을 제거하세요.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인 결과, 이 패턴은 프로젝트 전역 컨벤션이라 유지합니다.

@Resource(name = "...") + @RequiredArgsConstructor 병용은 현재 inbound 어댑터 컨트롤러 48개 파일에서 동일하게 사용 중입니다(예: UserSelfController, ManagerPostingController, AdminUserController 등). UseCase 빈이 @Service("...")로 명시적 이름을 갖는 구조와 짝을 이뤄 주입 대상을 빈 이름으로 고정하려는 의도적 관례입니다.

이 파일만 제거하면 코드베이스 일관성이 깨지므로 반영하지 않습니다. 패턴 자체의 재검토가 필요하다면 별도의 전역 리팩토링으로 다루는 것이 적절합니다.


@Override
@GetMapping
public ResponseEntity<CommonApiResponse<List<AdminPostingKeywordResponseDto>>> getPostingKeywordList() {
List<AdminPostingKeywordResponseDto> result = adminGetPostingKeywordList.execute().stream()
.map(AdminPostingKeywordResponseDto::from)
.toList();
return ResponseEntity.ok(CommonApiResponse.of(result));
}

@Override
@PostMapping
public ResponseEntity<CommonApiResponse<AdminPostingKeywordResponseDto>> createPostingKeyword(
@Valid @RequestBody AdminPostingKeywordRequestDto request
) {
PostingKeyword created = adminCreatePostingKeyword.execute(
new AdminCreatePostingKeywordCommand(request.getName(), request.getDescription())
);
return ResponseEntity.status(HttpStatus.CREATED)
.body(CommonApiResponse.of(AdminPostingKeywordResponseDto.from(created)));
}

@Override
@PutMapping("/{id}")
public ResponseEntity<CommonApiResponse<Void>> updatePostingKeyword(
@PathVariable Long id,
@Valid @RequestBody AdminPostingKeywordRequestDto request
) {
adminUpdatePostingKeyword.execute(
id, new AdminUpdatePostingKeywordCommand(request.getName(), request.getDescription())
);
return ResponseEntity.ok(CommonApiResponse.empty());
}

@Override
@DeleteMapping("/{id}")
public ResponseEntity<CommonApiResponse<Void>> deletePostingKeyword(
@PathVariable Long id
) {
adminDeletePostingKeyword.execute(id);
return ResponseEntity.ok(CommonApiResponse.empty());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.dreamteam.alter.adapter.inbound.admin.posting.controller;

import com.dreamteam.alter.adapter.inbound.admin.posting.dto.AdminPostingKeywordRequestDto;
import com.dreamteam.alter.adapter.inbound.admin.posting.dto.AdminPostingKeywordResponseDto;
import com.dreamteam.alter.adapter.inbound.common.dto.CommonApiResponse;
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 org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.List;

@Tag(name = "ADMIN - 업종(키워드) 관리 API")
public interface AdminPostingKeywordControllerSpec {

@Operation(summary = "업종 목록 조회", description = "관리자가 등록된 업종(키워드) 전체 목록을 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "업종 목록 조회 성공")
})
ResponseEntity<CommonApiResponse<List<AdminPostingKeywordResponseDto>>> getPostingKeywordList();

@Operation(summary = "업종 생성", description = "관리자가 새 업종(키워드)을 등록합니다. 이름은 중복될 수 없습니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "업종 생성 성공"),
@ApiResponse(responseCode = "400", description = "이름 중복 또는 유효성 검증 실패")
})
ResponseEntity<CommonApiResponse<AdminPostingKeywordResponseDto>> createPostingKeyword(
@Valid @RequestBody AdminPostingKeywordRequestDto request
);

@Operation(summary = "업종 수정", description = "관리자가 업종(키워드)의 이름/설명을 수정합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "업종 수정 성공"),
@ApiResponse(responseCode = "400", description = "이름 중복 또는 존재하지 않는 업종")
})
ResponseEntity<CommonApiResponse<Void>> updatePostingKeyword(
@PathVariable Long id,
@Valid @RequestBody AdminPostingKeywordRequestDto request
);

@Operation(summary = "업종 삭제", description = "관리자가 업종(키워드)을 삭제합니다. 공고에서 사용 중인 업종은 삭제할 수 없습니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "업종 삭제 성공"),
@ApiResponse(responseCode = "409", description = "공고에서 사용 중인 업종")
})
ResponseEntity<CommonApiResponse<Void>> deletePostingKeyword(
@PathVariable Long id
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.dreamteam.alter.adapter.inbound.admin.posting.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "업종(키워드) 생성/수정 요청 DTO")
public class AdminPostingKeywordRequestDto {

@NotBlank
@Size(max = 128)
@Schema(description = "업종명", example = "카페")
private String name;

@Size(max = 255)
@Schema(description = "업종 설명", example = "카페/디저트 매장")
private String description;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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 AdminPostingKeywordResponseDto {

@Schema(description = "업종 ID", example = "1")
private Long id;

@Schema(description = "업종명", example = "카페")
private String name;

@Schema(description = "업종 설명", example = "카페/디저트 매장")
private String description;

public static AdminPostingKeywordResponseDto from(PostingKeyword keyword) {
return AdminPostingKeywordResponseDto.builder()
.id(keyword.getId())
.name(keyword.getName())
.description(keyword.getDescription())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import com.dreamteam.alter.domain.posting.type.PaymentType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import lombok.*;
import org.apache.commons.lang3.ObjectUtils;

import java.util.List;

Expand Down Expand Up @@ -39,10 +41,13 @@ public class CreatePostingRequestDto {
@NotNull
private PaymentType paymentType;

@Schema(description = "키워드", example = "[2, 3, 1]")
@NotEmpty
@Schema(description = "마스터 업종 ID 목록 (선택)", example = "[2, 3, 1]")
private List<Long> keywords;

@Schema(description = "직접입력 업종 (마스터 미등록, 공고 라벨로 저장)", example = "[\"브런치카페\"]")
@Size(max = 10)
private List<@NotBlank @Size(max = 128) String> customKeywords;

@Schema(description = "공고 스케줄", example = "[" +
"{" +
"\"workingDays\": [\"MONDAY\", \"WEDNESDAY\"], \"startTime\": \"09:00\", \"endTime\": \"18:00\", \"positionsNeeded\": 3, \"position\": \"설거지\"" +
Expand All @@ -54,4 +59,10 @@ public class CreatePostingRequestDto {
@Valid
private List<CreatePostingScheduleRequestDto> schedules;

@Schema(hidden = true)
@AssertTrue(message = "업종은 최소 1개 이상 선택하거나 직접 입력해야 합니다.")
public boolean isKeywordProvided() {
return ObjectUtils.isNotEmpty(keywords) || ObjectUtils.isNotEmpty(customKeywords);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,13 @@ public class PostingDetailResponseDto {
private LocalDateTime createdAt;

@NotNull
@Schema(description = "키워드", example = "[{\"id\":1,\"name\":\"카페\"},{\"id\":4,\"name\":\"분식\"}]")
@Schema(description = "키워드(마스터 업종)", example = "[{\"id\":1,\"name\":\"카페\"},{\"id\":4,\"name\":\"분식\"}]")
private List<PostingKeywordListResponseDto> keywords;

@NotNull
@Schema(description = "직접입력 업종", example = "[\"브런치카페\"]")
private List<String> customKeywords;

@NotNull
@Schema(description = "공고 스케줄", example = "[{\"id\":1,\"workingDays\":[\"MONDAY\",\"WEDNESDAY\"],\"startTime\":\"09:00\",\"endTime\":\"18:00\",\"positionsNeeded\":3,\"positionsAvailable\":2,\"position\":\"홀서빙\"}]")
private List<PostingScheduleResponseDto> schedules;
Expand All @@ -77,6 +81,7 @@ public static PostingDetailResponseDto from(
.keywords(entity.getPostingKeywords().stream()
.map(PostingKeywordListResponseDto::from)
.toList())
.customKeywords(entity.getCustomKeywords())
.scrapped(entity.isScrapped())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dreamteam.alter.adapter.inbound.general.posting.dto;

import com.dreamteam.alter.adapter.inbound.common.dto.DescribedEnumDto;
import com.dreamteam.alter.domain.posting.entity.PostingKeyword;
import com.dreamteam.alter.domain.posting.type.PostingSortType;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
Expand All @@ -26,11 +27,15 @@ public class PostingFilterOptionsResponseDto {
@Schema(description = "정렬 옵션 목록")
private List<DescribedEnumDto<PostingSortType>> sortOptions;

@Schema(description = "업종(마스터) 추천 목록 — 필터 검색어 자동완성/칩 용도")
private List<PostingKeywordListResponseDto> keywords;

public static PostingFilterOptionsResponseDto of(
List<String> provinces,
List<String> districts,
List<String> towns,
List<PostingSortType> sortOptions
List<PostingSortType> sortOptions,
List<PostingKeyword> keywords
) {
return PostingFilterOptionsResponseDto.builder()
.provinces(provinces)
Expand All @@ -40,6 +45,10 @@ public static PostingFilterOptionsResponseDto of(
.map(sortType -> DescribedEnumDto.of(sortType, PostingSortType.describe()))
.toList()
)
.keywords(keywords.stream()
.map(PostingKeywordListResponseDto::from)
.toList()
)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,7 @@ public class PostingListFilterDto {

@Parameter(description = "급여순 정렬 여부")
private Boolean payAmountSort;

@Parameter(description = "업종 검색어 (마스터 업종명 또는 직접입력 업종 부분일치)")
private String keyword;
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ public class PostingListResponseDto {
private LocalDateTime createdAt;

@NotNull
@Schema(description = "키워드", example = "[{\"id\":1,\"name\":\"카페\"},{\"id\":4,\"name\":\"분식\"}]")
@Schema(description = "키워드(마스터 업종)", example = "[{\"id\":1,\"name\":\"카페\"},{\"id\":4,\"name\":\"분식\"}]")
private List<PostingKeywordListResponseDto> keywords;

@NotNull
@Schema(description = "직접입력 업종", example = "[\"브런치카페\"]")
private List<String> customKeywords;

@NotNull
@Schema(description = "공고 스케줄", example = "[" +
"{" +
Expand Down Expand Up @@ -73,6 +77,7 @@ public static PostingListResponseDto from(PostingListResponse entity) {
.keywords(entity.getPostingKeywords().stream()
.map(PostingKeywordListResponseDto::from)
.toList())
.customKeywords(entity.getCustomKeywords())
.workspace(PostingListWorkspaceResponseDto.from(entity.getWorkspace()))
.scrapped(entity.isScrapped())
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,13 @@ public class ManagerPostingDetailResponseDto {
private LocalDateTime updatedAt;

@NotNull
@Schema(description = "키워드", example = "[{\"id\":1,\"name\":\"카페\"},{\"id\":4,\"name\":\"분식\"}]")
@Schema(description = "키워드(마스터 업종)", example = "[{\"id\":1,\"name\":\"카페\"},{\"id\":4,\"name\":\"분식\"}]")
private List<PostingKeywordListResponseDto> keywords;

@NotNull
@Schema(description = "직접입력 업종", example = "[\"브런치카페\"]")
private List<String> customKeywords;

@NotNull
@Schema(description = "공고 스케줄", example = "[{\"id\":1,\"workingDays\":[\"MONDAY\",\"WEDNESDAY\"],\"startTime\":\"09:00\",\"endTime\":\"18:00\",\"positionsNeeded\":3,\"positionsAvailable\":2,\"position\":\"홀서빙\"}]")
private List<PostingScheduleResponseDto> schedules;
Expand All @@ -83,6 +87,7 @@ public static ManagerPostingDetailResponseDto from(ManagerPostingDetailResponse
.keywords(entity.getPostingKeywords().stream()
.map(PostingKeywordListResponseDto::from)
.toList())
.customKeywords(entity.getCustomKeywords())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,13 @@ public class ManagerPostingListResponseDto {
private LocalDateTime createdAt;

@NotNull
@Schema(description = "키워드", example = "[{\"id\":1,\"name\":\"카페\"},{\"id\":4,\"name\":\"분식\"}]")
@Schema(description = "키워드(마스터 업종)", example = "[{\"id\":1,\"name\":\"카페\"},{\"id\":4,\"name\":\"분식\"}]")
private List<PostingKeywordListResponseDto> keywords;

@NotNull
@Schema(description = "직접입력 업종", example = "[\"브런치카페\"]")
private List<String> customKeywords;

@NotNull
@Schema(description = "공고 스케줄", example = "[" +
"{" +
Expand All @@ -69,6 +73,7 @@ public static ManagerPostingListResponseDto from(ManagerPostingListResponse resp
.keywords(response.getPostingKeywords().stream()
.map(PostingKeywordListResponseDto::from)
.toList())
.customKeywords(response.getCustomKeywords())
.schedules(response.getSchedules().stream()
.map(PostingScheduleResponseDto::from)
.toList())
Expand Down
Loading
Loading