Skip to content
Merged
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 @@ -11,7 +11,7 @@
@Getter @Setter
@NoArgsConstructor
public class JobAlias extends BaseEntity {
@Column(name = "name", nullable = false)
@Column(name = "name", nullable = false, unique = true)
private String name; // 사용자가 입력한 직군 이름

@ManyToOne(fetch = FetchType.LAZY)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.back.domain.job.job.repository;

import com.back.domain.job.job.entity.JobAlias;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.Optional;

public interface JobAliasRepository extends JpaRepository<JobAlias, Long> {
Optional<JobAlias> findByName(String name);
Optional<JobAlias> findByNameIgnoreCase(String name);
List<JobAlias> findByJobIsNull(); // pending 상태인 alias 조회
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.back.domain.job.job.repository;

import com.back.domain.job.job.entity.Job;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface JobRepository extends JpaRepository<Job, Long> {
Optional<Job> findByName(String name);
}
34 changes: 34 additions & 0 deletions back/src/main/java/com/back/domain/job/job/service/JobService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.back.domain.job.job.service;

import com.back.domain.job.job.entity.Job;
import com.back.domain.job.job.entity.JobAlias;
import com.back.domain.job.job.repository.JobAliasRepository;
import com.back.domain.job.job.repository.JobRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class JobService {
private final JobRepository jobRepository;
private final JobAliasRepository jobAliasRepository;

public long count() {
return jobRepository.count();
}

@Transactional
public Job create(String name, String description) {
Job job = new Job(name, description);
return jobRepository.save(job);
}

@Transactional
public JobAlias createAlias(Job job, String aliasName) {
JobAlias alias = new JobAlias(aliasName);
alias.setJob(job);
return jobAliasRepository.save(alias);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.back.domain.roadmap.task.controller;

import com.back.domain.member.member.entity.Member;
import com.back.domain.roadmap.task.dto.*;
import com.back.domain.roadmap.task.entity.Task;
import com.back.domain.roadmap.task.entity.TaskAlias;
import com.back.domain.roadmap.task.service.TaskService;
import com.back.global.exception.ServiceException;
import com.back.global.rq.Rq;
import com.back.global.rsData.RsData;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;

import java.util.Collections;
import java.util.List;

@RestController
@RequestMapping("/api/tasks")
@RequiredArgsConstructor
public class TaskController {
private final TaskService taskService;
private final Rq rq;

// 검색 api
@GetMapping("/search")
public RsData<List<TaskDto>> searchTasks(@RequestParam String keyword) {
// 입력값 검증
if (keyword == null || keyword.trim().isEmpty()) {
return new RsData<>(
"200",
"검색 결과가 없습니다.",
Collections.emptyList()
);
}

List<Task> tasks = taskService.searchByKeyword(keyword.trim());
List<TaskDto> responses = tasks.stream()
.map(TaskDto::new)
.toList();

return new RsData<>(
"200",
"Task 검색 성공",
responses
);
}

// 사용자가 새로운 기술 제안 (pending alias 생성)
@PostMapping("/aliases/pending")
public RsData<CreatePendingAliasResponse> createPendingAlias(@Valid @RequestBody CreatePendingAliasRequest request) {
TaskAlias pendingAlias = taskService.createPendingAlias(request.taskName().trim());

return new RsData<>(
"201",
"새로운 Pending Alias 등록 성공. 관리자 검토 후 매칭 또는 새로운 Task로 등록됩니다.",
new CreatePendingAliasResponse(pendingAlias)
);
}


//=== 관리자용 API ===
// 나중에 CORS 설정
@GetMapping("/aliases/pending")
public RsData<Page<TaskAliasDto>> getPendingTaskAliases(
@PageableDefault(size = 10, sort = "createdDate", direction = Sort.Direction.DESC) Pageable pageable
) {
validateAdminRole();

Page<TaskAliasDto> pendingTaskAliases = taskService.getPendingTaskAliases(pageable);

return new RsData<>(
"200",
"pending 상태의 TaskAlias 목록 조회 성공",
pendingTaskAliases
);
}

// Pending alias를 기존 Task와 연결
@PutMapping("/aliases/pending/{aliasId}/link")
public RsData<TaskAliasDetailDto> linkPendingAlias(
@PathVariable Long aliasId,
@Valid @RequestBody LinkPendingAliasRequest request
) {
validateAdminRole();

TaskAlias linkedAlias = taskService.linkPendingAlias(aliasId, request.taskId());

return new RsData<>(
"200",
"Pending Alias를 Task와 연결 성공",
new TaskAliasDetailDto(linkedAlias)
);
}

// Pending alias를 새로운 Task로 등록(생성)
@PostMapping("/aliases/pending/{aliasId}")
public RsData<TaskDto> createTaskFromPending(
@PathVariable Long aliasId
) {
validateAdminRole();

Task newTask = taskService.createTaskFromPending(aliasId);

return new RsData<>(
"201",
"Pending Alias를 새로운 Task로 등록 성공",
new TaskDto(newTask)
);
}

@DeleteMapping("/aliases/pending/{aliasId}")
public RsData<Void> deletePendingAlias(@PathVariable Long aliasId) {
validateAdminRole();
taskService.deletePendingAlias(aliasId);
return new RsData<>("200", "Pending Alias 삭제 성공", null);
}

private void validateAdminRole() {
Member member = rq.getActor();
if(member == null) {
throw new ServiceException("401", "로그인 후 이용해주세요.");
}
if(member.getRole() != Member.Role.ADMIN){
throw new ServiceException("403", "권한이 없습니다.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.back.domain.roadmap.task.dto;

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

public record CreatePendingAliasRequest(
@NotBlank(message = "기술명은 필수입니다")
@Size(min = 1, max = 50, message = "기술명은 1-50자 사이여야 합니다")
String taskName
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.back.domain.roadmap.task.dto;

import com.back.domain.roadmap.task.entity.TaskAlias;

import java.time.LocalDateTime;

public record CreatePendingAliasResponse(
Long aliasId,
String aliasName,
LocalDateTime createTime
) {
public CreatePendingAliasResponse(TaskAlias alias){
this(
alias.getId(),
alias.getName(),
alias.getCreateDate()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.back.domain.roadmap.task.dto;

import jakarta.validation.constraints.NotNull;

public record LinkPendingAliasRequest(
@NotNull(message = "Task ID는 필수입니다.")
Long taskId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.back.domain.roadmap.task.dto;

import com.back.domain.roadmap.task.entity.TaskAlias;

public record TaskAliasDetailDto(
Long aliasId,
String aliasName,
Long taskId,
String taskName
) {
public TaskAliasDetailDto(TaskAlias alias){
this(
alias.getId(),
alias.getName(),
alias.getTask().getId(),
alias.getTask().getName()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.back.domain.roadmap.task.dto;

import com.back.domain.roadmap.task.entity.TaskAlias;

public record TaskAliasDto(
Long aliasId,
String aliasName
) {
public TaskAliasDto(TaskAlias alias){
this(
alias.getId(),
alias.getName()
);
}
}
12 changes: 12 additions & 0 deletions back/src/main/java/com/back/domain/roadmap/task/dto/TaskDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.back.domain.roadmap.task.dto;

import com.back.domain.roadmap.task.entity.Task;

public record TaskDto(
Long id,
String name
) {
public TaskDto(Task task){
this(task.getId(),task.getName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
@Getter @Setter
@NoArgsConstructor
public class TaskAlias extends BaseEntity {
@Column(name = "name", nullable = false)
@Column(name = "name", nullable = false, unique = true)
private String name; // 사용자가 입력한 Task 이름

@ManyToOne(fetch = FetchType.LAZY)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.back.domain.roadmap.task.repository;

import com.back.domain.roadmap.task.entity.TaskAlias;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface TaskAliasRepository extends JpaRepository<TaskAlias, Long> {
Optional<TaskAlias> findByNameIgnoreCase(String name);
Page<TaskAlias> findByTaskIsNull(Pageable pageable); // pending alias 목록 페이징 조회
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.back.domain.roadmap.task.repository;

import com.back.domain.roadmap.task.entity.Task;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;

public interface TaskRepository extends JpaRepository<Task, Long> {
Optional<Task> findByNameIgnoreCase(String name);

@Query("""
SELECT DISTINCT t FROM Task t
LEFT JOIN FETCH t.aliases ta
WHERE LOWER(t.name) LIKE LOWER(CONCAT('%', :keyword, '%'))
OR (ta.task IS NOT NULL AND LOWER(ta.name) LIKE LOWER(CONCAT('%', :keyword, '%')))
ORDER BY t.name
""")
List<Task> findTasksByKeyword(@Param("keyword") String keyword);
}
Loading