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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@ logs/
### Local config ###
application-local.yml
.env

# 정적 리소스
src/main/resources/static/
Empty file modified gradlew
100755 → 100644
Empty file.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.ilson.spotwork.common.exception;

import com.ilson.spotwork.common.response.ApiResponse;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
Expand All @@ -24,10 +25,7 @@ public ResponseEntity<ApiResponse<Void>> handleCustomException(CustomException e
log.error("CustomException: {}", errorCode.getMessage());
return ResponseEntity
.status(errorCode.getHttpStatus())
.body(ApiResponse.error(
errorCode.getHttpStatus().value(),
errorCode.getMessage()
));
.body(ApiResponse.error(errorCode.getMessage()));
}

// 이메일 확인 동시 요청으로 인한 DB 유니크 제약 위반 처리
Expand All @@ -36,10 +34,7 @@ public ResponseEntity<ApiResponse<Void>> handleDataIntegrityViolation(DataIntegr
log.error("DataIntegrityViolationException: {}", e.getMessage());
return ResponseEntity
.status(HttpStatus.CONFLICT)
.body(ApiResponse.error(
ErrorCode.DUPLICATE_EMAIL.getHttpStatus().value(),
ErrorCode.DUPLICATE_EMAIL.getMessage()
));
.body(ApiResponse.error(ErrorCode.DUPLICATE_EMAIL.getMessage()));
}

// Validation 에러 처리
Expand All @@ -53,7 +48,16 @@ public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationExceptio
log.error("ValidationException: {}", errors);
return ResponseEntity
.badRequest()
.body(ApiResponse.error(400, "입력값이 올바르지 않습니다.", errors));
.body(ApiResponse.error("입력값이 올바르지 않습니다.", errors));
}

// @Validated 파라미터 검증 실패 처리
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ApiResponse<Void>> handleConstraintViolation(ConstraintViolationException e) {
log.error("ConstraintViolationException: {}", e.getMessage());
return ResponseEntity
.badRequest()
.body(ApiResponse.error("입력값이 올바르지 않습니다."));
}

// 전체 예외 처리 (fallback)
Expand All @@ -62,6 +66,6 @@ public ResponseEntity<ApiResponse<Void>> handleException(Exception e) {
log.error("Unexpected Exception: {}", e.getMessage());
return ResponseEntity
.internalServerError()
.body(ApiResponse.error(500, "서버 오류가 발생했습니다."));
.body(ApiResponse.error("서버 오류가 발생했습니다."));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,27 @@
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiResponse<T> {

private int code;
private String message;
private T data;

private ApiResponse(int code, String message, T data) {
this.code = code;
private ApiResponse(String message, T data) {
this.message = message;
this.data = data;
}

public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "success", data);
return new ApiResponse<>("success", data);
}

public static <T> ApiResponse<T> success() {
return new ApiResponse<>(200, "success", null);
return new ApiResponse<>("success", null);
}

public static <T> ApiResponse<T> error(int code, String message) {
return new ApiResponse<>(code, message, null);
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(message, null);
}

public static <T> ApiResponse<T> error(int code, String message, T data) {
return new ApiResponse<>(code, message, data);
public static <T> ApiResponse<T> error(String message, T data) {
return new ApiResponse<>(message, data);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.ilson.spotwork.domain.job.controller;

import com.ilson.spotwork.common.response.ApiResponse;
import com.ilson.spotwork.domain.job.dto.JobCreateRequestDto;
import com.ilson.spotwork.domain.job.dto.JobResponseDto;
import com.ilson.spotwork.domain.job.dto.JobUpdateRequestDto;
import com.ilson.spotwork.domain.job.service.JobService;
import com.ilson.spotwork.infra.security.jwt.CustomUserDetails;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Positive;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@Validated
@RestController
@RequestMapping("/api/jobs")
@RequiredArgsConstructor
public class JobController {

private final JobService jobService;

// 공고 등록
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ApiResponse<JobResponseDto> register(
@AuthenticationPrincipal CustomUserDetails userDetails,
@RequestBody @Valid JobCreateRequestDto request) {
JobResponseDto response = jobService.register(userDetails.getUserId(), request);
return ApiResponse.success(response);
Comment thread
kosy00 marked this conversation as resolved.
Comment thread
kosy00 marked this conversation as resolved.
}

// 공고 목록 조회
@GetMapping
public ApiResponse<Slice<JobResponseDto>> getList(
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "10") @Positive int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"));
Slice<JobResponseDto> response = jobService.getList(pageable);
return ApiResponse.success(response);
}

// 공고 상세 조회
@GetMapping("/{jobId}")
public ApiResponse<JobResponseDto> getDetail(@PathVariable Long jobId) {
JobResponseDto response = jobService.getDetail(jobId);
return ApiResponse.success(response);
}

// 공고 수정
@PatchMapping("/{jobId}")
public ApiResponse<JobResponseDto> update(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long jobId,
@RequestBody @Valid JobUpdateRequestDto request) {
return ApiResponse.success(jobService.update(userDetails.getUserId(), jobId, request));
}

// 공고 삭제
@DeleteMapping("/{jobId}")
public ApiResponse<Void> delete(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long jobId) {
jobService.delete(userDetails.getUserId(), jobId);
return ApiResponse.success();
}

// 내 공고 목록 조회
@GetMapping("/me")
public ApiResponse<Slice<JobResponseDto>> getMyJobs(
@AuthenticationPrincipal CustomUserDetails userDetails,
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "10") @Positive int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"));
return ApiResponse.success(jobService.getMyJobs(userDetails.getUserId(), pageable));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.ilson.spotwork.domain.job.dto;

import com.ilson.spotwork.domain.job.entity.JobCategory;
import jakarta.validation.constraints.*;
import lombok.Getter;

import java.time.LocalDate;
import java.time.LocalTime;

@Getter
public class JobCreateRequestDto {

@NotBlank(message = "제목은 필수입니다.")
private String title;

private String description;

@NotNull(message = "업종은 필수입니다.")
private JobCategory category;

@Min(value = 10030, message = "최저시급 이상이어야 합니다.")
private int hourlyWage;

@NotNull(message = "근무 날짜는 필수입니다.")
@FutureOrPresent(message = "근무 날짜는 오늘 이후여야 합니다.")
private LocalDate workDate;

@NotNull(message = "시작 시간은 필수입니다.")
private LocalTime startTime;

@NotNull(message = "종료 시간은 필수입니다.")
private LocalTime endTime;
Comment thread
kosy00 marked this conversation as resolved.

@AssertTrue(message = "종료 시간은 시작 시간보다 늦어야 합니다.")
private boolean isValidTimeRange() {
return startTime == null || endTime == null || endTime.isAfter(startTime);
}

@Min(value = 1, message = "모집 인원은 1명 이상이어야 합니다.")
private int headcount;

@NotBlank(message = "주소는 필수입니다.")
private String address;

@NotNull(message = "위도는 필수입니다.")
@DecimalMin(value = "-90.0", message = "위도는 -90 이상이어야 합니다.")
@DecimalMax(value = "90.0", message = "위도는 90 이하여야 합니다.")
private Double latitude;

@NotNull(message = "경도는 필수입니다.")
@DecimalMin(value = "-180.0", message = "경도는 -180 이상이어야 합니다.")
@DecimalMax(value = "180.0", message = "경도는 180 이하여야 합니다.")
private Double longitude;
Comment thread
kosy00 marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.ilson.spotwork.domain.job.dto;

import com.ilson.spotwork.domain.job.entity.Job;
import com.ilson.spotwork.domain.job.entity.JobCategory;
import com.ilson.spotwork.domain.job.entity.JobStatus;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDate;
import java.time.LocalTime;

@Getter
@Builder
public class JobResponseDto {

private Long id;
private String title;
private String description;
private JobCategory category;
private int hourlyWage;
private LocalDate workDate;
private LocalTime startTime;
private LocalTime endTime;
private int headcount;
private String address;
private double latitude;
private double longitude;
private JobStatus status;
private EmployerInfo employer;

// 사업자 정보 (중첩 DTO)
@Getter
@Builder
public static class EmployerInfo {
private Long id;
private String nickname;
private Double avgRating;
}

public static JobResponseDto from(Job job) {
return JobResponseDto.builder()
.id(job.getId())
.title(job.getTitle())
.description(job.getDescription())
.category(job.getCategory())
.hourlyWage(job.getHourlyWage())
.workDate(job.getWorkDate())
.startTime(job.getStartTime())
.endTime(job.getEndTime())
.headcount(job.getHeadcount())
.address(job.getAddress())
.latitude(job.getLatitude())
.longitude(job.getLongitude())
.status(job.getStatus())
.employer(EmployerInfo.builder()
.id(job.getEmployer().getId())
.nickname(job.getEmployer().getNickname())
.avgRating(job.getEmployer().getAvgRating() != null
? job.getEmployer().getAvgRating().doubleValue()
: null)
.build())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.ilson.spotwork.domain.job.dto;

import jakarta.validation.constraints.*;

import lombok.Getter;

import java.time.LocalDate;
import java.time.LocalTime;

@Getter
public class JobUpdateRequestDto {

@NotBlank(message = "제목은 필수입니다.")
private String title;

private String description;

@Min(value = 10030, message = "최저시급 이상이어야 합니다.")
private int hourlyWage;

@NotNull
@FutureOrPresent(message = "근무 날짜는 오늘 이후여야 합니다.")
private LocalDate workDate;
Comment thread
kosy00 marked this conversation as resolved.

@NotNull
private LocalTime startTime;

@NotNull
private LocalTime endTime;

@Min(value = 1, message = "모집 인원은 1명 이상이어야 합니다.")
private int headcount;

@NotBlank(message = "주소는 필수입니다.")
private String address;

@NotNull(message = "위도는 필수입니다.")
@DecimalMin(value = "-90.0", message = "위도는 -90 이상이어야 합니다.")
@DecimalMax(value = "90.0", message = "위도는 90 이하여야 합니다.")
private Double latitude;

@NotNull(message = "경도는 필수입니다.")
@DecimalMin(value = "-180.0", message = "경도는 -180 이상이어야 합니다.")
@DecimalMax(value = "180.0", message = "경도는 180 이하여야 합니다.")
private Double longitude;
}
Loading
Loading