Skip to content

Commit 0a36e4d

Browse files
authored
Merge pull request #24 from 9oormthon-univ/feat/disposal
[Feat] 오늘의 분리수거 조회 구현
2 parents 90a96eb + c1f723c commit 0a36e4d

File tree

10 files changed

+233
-4
lines changed

10 files changed

+233
-4
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.trashheroesbe.feature.disposal.api;
2+
3+
import static com.trashheroesbe.global.response.type.SuccessCode.OK;
4+
5+
import com.trashheroesbe.feature.disposal.application.DisposalService;
6+
import com.trashheroesbe.feature.disposal.dto.response.DistrictTrashDisposalResponse;
7+
import com.trashheroesbe.global.auth.security.CustomerDetails;
8+
import com.trashheroesbe.global.response.ApiResponse;
9+
import java.util.List;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
12+
import org.springframework.web.bind.annotation.GetMapping;
13+
import org.springframework.web.bind.annotation.RequestMapping;
14+
import org.springframework.web.bind.annotation.RestController;
15+
16+
@RestController
17+
@RequiredArgsConstructor
18+
@RequestMapping("/api/v1/disposals")
19+
public class DisposalController implements DisposalControllerApi {
20+
21+
private final DisposalService disposalService;
22+
23+
@Override
24+
@GetMapping("/today")
25+
public ApiResponse<List<DistrictTrashDisposalResponse>> getTodayTrashDisposal(
26+
@AuthenticationPrincipal CustomerDetails customerDetails
27+
) {
28+
List<DistrictTrashDisposalResponse> response = disposalService.getTodayTrashDisposal(
29+
customerDetails.getUser().getId());
30+
return ApiResponse.success(OK, response);
31+
}
32+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.trashheroesbe.feature.disposal.api;
2+
3+
import com.trashheroesbe.feature.disposal.dto.response.DistrictTrashDisposalResponse;
4+
import com.trashheroesbe.feature.user.dto.response.UserDistrictResponse;
5+
import com.trashheroesbe.global.auth.security.CustomerDetails;
6+
import com.trashheroesbe.global.response.ApiResponse;
7+
import io.swagger.v3.oas.annotations.Operation;
8+
import io.swagger.v3.oas.annotations.tags.Tag;
9+
import java.util.List;
10+
11+
@Tag(name = "Disposal", description = "자치구별 배출 관련 API")
12+
public interface DisposalControllerApi {
13+
14+
@Operation(summary = "오늘의 분리수거 조회", description = "나의 등록된 자치구의 분리수거 정보를 조회 합니다.")
15+
ApiResponse<List<DistrictTrashDisposalResponse>> getTodayTrashDisposal(
16+
CustomerDetails customerDetails
17+
);
18+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.trashheroesbe.feature.disposal.application;
2+
3+
import static com.trashheroesbe.global.response.type.ErrorCode.NOT_FOUND_DEFAULT_USER_DISTRICTS;
4+
import static com.trashheroesbe.global.response.type.ErrorCode.NOT_FOUND_USER_DISTRICTS;
5+
6+
import com.trashheroesbe.feature.disposal.domain.Disposal;
7+
import com.trashheroesbe.feature.disposal.dto.response.DistrictTrashDisposalResponse;
8+
import com.trashheroesbe.feature.disposal.infrastructure.DisposalRepository;
9+
import com.trashheroesbe.feature.user.domain.entity.UserDistrict;
10+
import com.trashheroesbe.feature.user.domain.service.UserDistrictFinder;
11+
import com.trashheroesbe.global.exception.BusinessException;
12+
import com.trashheroesbe.global.util.DateTimeUtils;
13+
import java.time.LocalDate;
14+
import java.util.List;
15+
import lombok.RequiredArgsConstructor;
16+
import org.springframework.stereotype.Service;
17+
import org.springframework.transaction.annotation.Transactional;
18+
19+
@Service
20+
@Transactional(readOnly = true)
21+
@RequiredArgsConstructor
22+
public class DisposalService {
23+
24+
private final UserDistrictFinder userDistrictFinder;
25+
private final DisposalRepository disposalRepository;
26+
27+
public List<DistrictTrashDisposalResponse> getTodayTrashDisposal(Long userId) {
28+
List<UserDistrict> userDistricts = userDistrictFinder.findByUserId(userId);
29+
UserDistrict defaultUserDistrict = userDistricts.stream()
30+
.filter(UserDistrict::getIsDefault)
31+
.findFirst()
32+
.orElseThrow(() -> userDistricts.isEmpty()
33+
? new BusinessException(NOT_FOUND_USER_DISTRICTS)
34+
: new BusinessException(NOT_FOUND_DEFAULT_USER_DISTRICTS)
35+
);
36+
37+
String todayDay = DateTimeUtils.getTodayKorean();
38+
LocalDate todayDate = DateTimeUtils.getTodayDate();
39+
List<Disposal> disposals = disposalRepository.findAllByDistrictAndDay(
40+
defaultUserDistrict.getDistrict().getId(),
41+
todayDay
42+
);
43+
44+
return DistrictTrashDisposalResponse.of(disposals, todayDay, todayDate);
45+
}
46+
}

src/main/java/com/trashheroesbe/feature/disposal/domain/Disposal.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212
import jakarta.persistence.JoinColumn;
1313
import jakarta.persistence.ManyToOne;
1414
import jakarta.persistence.Table;
15+
import java.util.List;
1516
import lombok.AccessLevel;
1617
import lombok.AllArgsConstructor;
1718
import lombok.Builder;
1819
import lombok.Getter;
1920
import lombok.NoArgsConstructor;
21+
import org.hibernate.annotations.JdbcTypeCode;
22+
import org.hibernate.type.SqlTypes;
2023

2124
@Getter
2225
@Entity
@@ -42,6 +45,7 @@ public class Disposal extends BaseTimeEntity {
4245
@Column(name = "method_detail", columnDefinition = "TEXT")
4346
private String methodDetail;
4447

48+
@JdbcTypeCode(SqlTypes.JSON)
4549
@Column(name = "days", columnDefinition = "JSON")
46-
private String days;
50+
private List<String> days;
4751
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package com.trashheroesbe.feature.disposal.dto.response;
2+
3+
import com.trashheroesbe.feature.disposal.domain.Disposal;
4+
import com.trashheroesbe.feature.trash.domain.Type;
5+
import java.time.LocalDate;
6+
import java.util.List;
7+
import java.util.Map;
8+
import java.util.stream.Collectors;
9+
import lombok.Builder;
10+
11+
@Builder
12+
public record DistrictTrashDisposalResponse(
13+
String categoryName,
14+
List<String> trashTypes,
15+
String location,
16+
String todayDay,
17+
LocalDate todayDate
18+
) {
19+
20+
public static List<DistrictTrashDisposalResponse> of(
21+
List<Disposal> disposals,
22+
String todayDay,
23+
LocalDate todayDate
24+
) {
25+
if (disposals.isEmpty()) {
26+
return List.of();
27+
}
28+
29+
String location = disposals.getFirst().getDistrict().getSigungu();
30+
31+
// 카테고리별로 그룹핑
32+
Map<String, List<String>> grouped = disposals.stream()
33+
.collect(Collectors.groupingBy(
34+
disposal -> getCategoryName(disposal.getTrashType().getType()),
35+
Collectors.mapping(
36+
disposal -> disposal.getTrashType().getType().getNameKo(),
37+
Collectors.toList()
38+
)
39+
));
40+
41+
return grouped.entrySet().stream()
42+
.map(entry -> DistrictTrashDisposalResponse.builder()
43+
.categoryName(entry.getKey())
44+
.trashTypes(entry.getValue())
45+
.location(location)
46+
.todayDay(todayDay)
47+
.todayDate(todayDate)
48+
.build())
49+
.sorted((a, b) -> getCategoryOrder(a.categoryName) - getCategoryOrder(b.categoryName))
50+
.toList();
51+
}
52+
53+
private static String getCategoryName(Type type) {
54+
return switch (type) {
55+
case NON_RECYCLABLE, FOOD_WASTE -> "일반쓰레기/음식물쓰레기";
56+
case PET, VINYL_FILM -> "투명페트병/비닐류";
57+
case PAPER, PAPER_PACK, PLASTIC, STYROFOAM, GLASS, METAL, TEXTILES,
58+
E_WASTE, HAZARDOUS_SMALL_WASTE -> "그외 재활용품";
59+
default -> "기타";
60+
};
61+
}
62+
63+
private static int getCategoryOrder(String categoryName) {
64+
return switch (categoryName) {
65+
case "일반쓰레기/음식물쓰레기" -> 1;
66+
case "투명페트병/비닐류" -> 2;
67+
case "그외 재활용품" -> 3;
68+
default -> 4;
69+
};
70+
}
71+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.trashheroesbe.feature.disposal.infrastructure;
2+
3+
import com.trashheroesbe.feature.disposal.domain.Disposal;
4+
import com.trashheroesbe.feature.district.domain.entity.District;
5+
import java.util.List;
6+
import org.springframework.data.jpa.repository.EntityGraph;
7+
import org.springframework.data.jpa.repository.Query;
8+
import org.springframework.data.repository.CrudRepository;
9+
import org.springframework.data.repository.query.Param;
10+
11+
public interface DisposalRepository extends CrudRepository<Disposal, Long> {
12+
13+
@EntityGraph(attributePaths = {"district", "trashType"})
14+
List<Disposal> findByDistrict(District district);
15+
16+
@EntityGraph(attributePaths = {"district", "trashType"})
17+
@Query("""
18+
select d
19+
from Disposal d
20+
where d.district.id = :districtId
21+
and function('json_overlaps', d.days, function('json_array', :day)) = true
22+
""")
23+
List<Disposal> findAllByDistrictAndDay(
24+
@Param("districtId") String districtId,
25+
@Param("day") String day
26+
);
27+
}

src/main/java/com/trashheroesbe/feature/user/api/UserController.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import static com.trashheroesbe.global.response.type.SuccessCode.OK;
44

5-
import com.trashheroesbe.feature.district.dto.response.DistrictListResponse;
65
import com.trashheroesbe.feature.user.application.UserService;
76
import com.trashheroesbe.feature.user.dto.request.UpdateUserRequest;
87
import com.trashheroesbe.feature.user.dto.response.UserDistrictResponse;
@@ -18,7 +17,6 @@
1817
import org.springframework.web.bind.annotation.PatchMapping;
1918
import org.springframework.web.bind.annotation.PathVariable;
2019
import org.springframework.web.bind.annotation.PostMapping;
21-
import org.springframework.web.bind.annotation.RequestBody;
2220
import org.springframework.web.bind.annotation.RequestMapping;
2321
import org.springframework.web.bind.annotation.RequestPart;
2422
import org.springframework.web.bind.annotation.RestController;

src/main/java/com/trashheroesbe/global/auth/jwt/entity/TokenType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
@Getter
77
@RequiredArgsConstructor
88
public enum TokenType {
9-
ACCESS_TOKEN("access_token", 1_800_000L), // 30분
9+
ACCESS_TOKEN("access_token", 10_800_000L), // 3시간
1010
REFRESH_TOKEN("refresh_token", 1_209_600_000L); // 2주
1111

1212
private final String name;

src/main/java/com/trashheroesbe/global/response/type/ErrorCode.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,16 @@ public enum ErrorCode {
2626
// user
2727
DUPLICATE_USER_DISTRICT(HttpStatus.BAD_REQUEST, "중복된 자치구 입니다."),
2828
MAX_USER_DISTRICTS_EXCEEDED(HttpStatus.BAD_REQUEST, "자치구는 최대 2개만 가질 수 있습니다."),
29+
NOT_FOUND_USER_DISTRICTS(HttpStatus.NOT_FOUND, "자치구 등록을 하지 않으셨습니다. 등록해주세요."),
30+
NOT_FOUND_DEFAULT_USER_DISTRICTS(HttpStatus.NOT_FOUND, "대표 유저 자치구를 찾을 수 없습니다."),
2931

3032
// trash
3133
NOT_EXISTS_TRASH_TYPE(HttpStatus.INTERNAL_SERVER_ERROR, "쓰레기 타입이 존재하지 않습니다. 개발자에게 문의 주세요."),
3234
NOT_EXISTS_TRASH_ITEM(HttpStatus.INTERNAL_SERVER_ERROR, "쓰레기 품목이 존재하지 않습니다. 개발자에게 문의 주세요."),
3335
NOT_EXISTS_TRASH_DESCRIPTION(HttpStatus.INTERNAL_SERVER_ERROR, "쓰레기 설명이 존재하지 않습니다. 개발자에게 문의 주세요."),
3436
INVALID_SEARCH_KEYWORD(HttpStatus.BAD_REQUEST, "검색 키워드가 비어있습니다."),
3537

38+
// gpt
3639
ERROR_GPT_CALL(HttpStatus.BAD_GATEWAY, "GPT 호출에 실패했습니다."),
3740
EMPTY_GPT_RESPONSE(HttpStatus.BAD_GATEWAY, "GPT 응답이 비었습니다."),
3841
FAIL_PARSING_RESPONSE(HttpStatus.UNPROCESSABLE_ENTITY, "응답 파싱에 실패했습니다"),
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.trashheroesbe.global.util;
2+
3+
import java.time.DayOfWeek;
4+
import java.time.LocalDate;
5+
import java.time.ZoneId;
6+
7+
public class DateTimeUtils {
8+
9+
public static final ZoneId KST = ZoneId.of("Asia/Seoul");
10+
11+
public static String getTodayKorean() {
12+
return toKoreanDay(LocalDate.now(KST).getDayOfWeek());
13+
}
14+
15+
public static LocalDate getTodayDate() {
16+
return LocalDate.now(KST);
17+
}
18+
19+
public static String toKoreanDay(DayOfWeek dayOfWeek) {
20+
return switch (dayOfWeek) {
21+
case MONDAY -> "월요일";
22+
case TUESDAY -> "화요일";
23+
case WEDNESDAY -> "수요일";
24+
case THURSDAY -> "목요일";
25+
case FRIDAY -> "금요일";
26+
case SATURDAY -> "토요일";
27+
case SUNDAY -> "일요일";
28+
};
29+
}
30+
}

0 commit comments

Comments
 (0)