Skip to content

Commit d6288b9

Browse files
committed
feat(car-model): 차량 모델 및 부품 카테고리 soft delete 구현
1 parent ee4acc6 commit d6288b9

8 files changed

Lines changed: 79 additions & 22 deletions

File tree

src/main/java/com/gearfirst/warehouse/api/parts/PcmController.java

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.gearfirst.warehouse.api.parts.persistence.entity.CarModelEntity;
1313
import com.gearfirst.warehouse.api.parts.service.PartCarModelService;
1414
import com.gearfirst.warehouse.common.exception.ConflictException;
15+
import com.gearfirst.warehouse.common.exception.NotFoundException;
1516
import com.gearfirst.warehouse.common.response.CommonApiResponse;
1617
import com.gearfirst.warehouse.common.response.PageEnvelope;
1718
import com.gearfirst.warehouse.common.response.SuccessStatus;
@@ -133,7 +134,7 @@ private Sort parseCarModelSort(List<String> sortParams) {
133134
return order.ignoreCase();
134135
}).toList();
135136
// apply whitelist
136-
List<String> allowed = java.util.List.of("name", "createdAt", "updatedAt");
137+
List<String> allowed = List.of("name", "createdAt", "updatedAt");
137138
List<Sort.Order> filtered = orders.stream()
138139
.filter(o -> allowed.contains(o.getProperty()))
139140
.toList();
@@ -221,25 +222,28 @@ public ResponseEntity<CommonApiResponse<Map<String, Boolean>>> deleteMapping(
221222
@PathVariable Long carModelId
222223
) {
223224
pcmService.deleteMapping(partId, carModelId);
224-
return CommonApiResponse.success(SuccessStatus.SEND_PCM_DELETE_SUCCESS, java.util.Map.of("deleted", true));
225+
return CommonApiResponse.success(SuccessStatus.SEND_PCM_DELETE_SUCCESS, Map.of("deleted", true));
225226
}
226227

227-
@Operation(summary = "차량 모델 삭제(soft)", description = "차량 모델이 활성 매핑에 참조 중이면 409를 반환합니다. 성공 시 enabled=false로 비활성화 처리합니다.")
228-
@DeleteMapping("/car-models/{id}")
229-
public ResponseEntity<CommonApiResponse<Map<String, Boolean>>> deleteCarModel(
228+
@Operation(summary = "차량 모델 활성 상태 토글", description = "차량 모델이 활성중에는 매핑에 참조 중이면 409를 반환, 성공 시 enabled=false로 비활성화 처리합니다. 비활성화 상태에서는 활성화 시킵니다.")
229+
@PatchMapping("/car-models/{id}/enable")
230+
public ResponseEntity<CommonApiResponse<Map<String, Boolean>>> toggleCarModelEnable(
230231
@PathVariable Long id
231232
) {
232233
var cm = carModelRepo.findById(id)
233-
.orElseThrow(() -> new com.gearfirst.warehouse.common.exception.NotFoundException("CarModel not found: " + id));
234+
.orElseThrow(() -> new NotFoundException("CarModel not found: " + id));
234235
long refCount = pcmRepo.countByCarModelIdAndEnabledTrue(id);
235236
if (refCount > 0) {
236-
throw new com.gearfirst.warehouse.common.exception.ConflictException(com.gearfirst.warehouse.common.response.ErrorStatus.CARMODEL_HAS_MAPPINGS);
237+
throw new ConflictException(ErrorStatus.CARMODEL_HAS_MAPPINGS);
237238
}
238239
if (cm.isEnabled()) {
239240
cm.setEnabled(false);
240241
carModelRepo.save(cm);
242+
} else {
243+
cm.setEnabled(true);
244+
carModelRepo.save(cm);
241245
}
242-
return CommonApiResponse.success(com.gearfirst.warehouse.common.response.SuccessStatus.SEND_CARMODEL_DELETE_SUCCESS,
243-
java.util.Map.of("deleted", true));
246+
return CommonApiResponse.success(SuccessStatus.SEND_CARMODEL_ENABLE_TOGGLED_SUCCESS,
247+
Map.of("toggled", true));
244248
}
245249
}

src/main/java/com/gearfirst/warehouse/api/parts/service/PartCarModelServiceImpl.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,12 @@ public void deleteMapping(Long partId, Long carModelId) {
149149
}
150150

151151
private PartCarModelDetail toDetail(PartCarModelEntity e) {
152-
String createdAt = e.getCreatedAt() != null ? e.getCreatedAt().toString() : null;
153-
String updatedAt = e.getUpdatedAt() != null ? e.getUpdatedAt().toString() : null;
152+
String createdAt = com.gearfirst.warehouse.common.util.DateTimes.toKstString(
153+
e.getCreatedAt() == null ? null : e.getCreatedAt().atOffset(java.time.ZoneOffset.UTC)
154+
);
155+
String updatedAt = com.gearfirst.warehouse.common.util.DateTimes.toKstString(
156+
e.getUpdatedAt() == null ? null : e.getUpdatedAt().atOffset(java.time.ZoneOffset.UTC)
157+
);
154158
return new PartCarModelDetail(e.getPartId(), e.getCarModelId(), e.getNote(), e.isEnabled(), createdAt,
155159
updatedAt);
156160
}

src/main/java/com/gearfirst/warehouse/api/parts/service/PartCategoryServiceImpl.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import com.gearfirst.warehouse.common.exception.ConflictException;
1212
import com.gearfirst.warehouse.common.exception.NotFoundException;
1313
import com.gearfirst.warehouse.common.response.ErrorStatus;
14+
import com.gearfirst.warehouse.common.util.DateTimes;
15+
import java.time.ZoneOffset;
1416
import java.util.List;
1517
import lombok.RequiredArgsConstructor;
1618
import org.springframework.stereotype.Service;
@@ -41,8 +43,12 @@ public List<CategorySummaryResponse> list(String keyword) {
4143
public CategoryDetailResponse get(Long id) {
4244
var c = categoryRepo.findById(id).orElseThrow(() -> new NotFoundException("Category not found: " + id));
4345
return new CategoryDetailResponse(c.getId(), c.getName(), c.getDescription(),
44-
c.getCreatedAt() != null ? c.getCreatedAt().toString() : null,
45-
c.getUpdatedAt() != null ? c.getUpdatedAt().toString() : null);
46+
DateTimes.toKstString(
47+
c.getCreatedAt() == null ? null : c.getCreatedAt().atOffset(ZoneOffset.UTC)
48+
),
49+
DateTimes.toKstString(
50+
c.getUpdatedAt() == null ? null : c.getUpdatedAt().atOffset(ZoneOffset.UTC)
51+
));
4652
}
4753

4854
@Override

src/main/java/com/gearfirst/warehouse/api/parts/service/PartServiceImpl.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,12 @@ private PartDetailResponse toDetail(PartEntity p) {
222222
p.getId(), p.getCode(), p.getName(), p.getPrice(),
223223
new CategoryRef(p.getCategoryId(), resolveCategoryName(p.getCategoryId())),
224224
p.getImageUrl(), p.isEnabled(),
225-
p.getCreatedAt() != null ? p.getCreatedAt().toString() : null,
226-
p.getUpdatedAt() != null ? p.getUpdatedAt().toString() : null,
225+
com.gearfirst.warehouse.common.util.DateTimes.toKstString(
226+
p.getCreatedAt() == null ? null : p.getCreatedAt().atOffset(java.time.ZoneOffset.UTC)
227+
),
228+
com.gearfirst.warehouse.common.util.DateTimes.toKstString(
229+
p.getUpdatedAt() == null ? null : p.getUpdatedAt().atOffset(java.time.ZoneOffset.UTC)
230+
),
227231
p.getSafetyStockQty()
228232
);
229233
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.gearfirst.warehouse.common.config;
2+
3+
import org.springframework.context.annotation.Configuration;
4+
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
5+
6+
@Configuration
7+
@EnableJpaAuditing
8+
public class JpaAuditingConfig {
9+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.gearfirst.warehouse.common.config;
2+
3+
import com.fasterxml.jackson.databind.SerializationFeature;
4+
import java.util.TimeZone;
5+
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
6+
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer;
7+
import org.springframework.context.annotation.Bean;
8+
import org.springframework.context.annotation.Configuration;
9+
10+
/**
11+
* Time policy configuration without touching application.yml.
12+
* - API I/O (Jackson): Asia/Seoul (KST), no timestamp numbers
13+
* - DB session (Hibernate): UTC
14+
*/
15+
@Configuration
16+
public class TimezoneConfig {
17+
18+
@Bean
19+
public Jackson2ObjectMapperBuilderCustomizer kstJacksonCustomizer() {
20+
return builder -> {
21+
builder.timeZone(TimeZone.getTimeZone("Asia/Seoul"));
22+
builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
23+
};
24+
}
25+
26+
@Bean
27+
public HibernatePropertiesCustomizer hibernateUtcCustomizer() {
28+
return (props) -> props.put("hibernate.jdbc.time_zone", "UTC");
29+
}
30+
}

src/main/java/com/gearfirst/warehouse/common/response/SuccessStatus.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public enum SuccessStatus {
4040

4141
// CarModel
4242
SEND_CARMODEL_CREATE_SUCCESS(HttpStatus.OK, "차량 모델 생성 성공"),
43-
SEND_CARMODEL_DELETE_SUCCESS(HttpStatus.OK, "차량 모델 삭제 성공"),
43+
SEND_CARMODEL_ENABLE_TOGGLED_SUCCESS(HttpStatus.OK, "차량 모델 상태 변경 성공"),
4444

4545
// PCM (Part–CarModel mapping)
4646
SEND_PCM_CARMODEL_LIST_SUCCESS(HttpStatus.OK, "부품 적용 차량 모델 목록 조회 성공"),

src/test/java/com/gearfirst/warehouse/api/parts/controller/PcmControllerMutationTest.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,28 +92,28 @@ void deleteMapping_success() throws Exception {
9292
}
9393

9494
@Test
95-
@DisplayName("DELETE /api/v1/car-models/{id} - 활성 매핑이 있으면 409")
95+
@DisplayName("PATCH /api/v1/car-models/{id} - 활성 매핑이 있으면 409")
9696
void deleteCarModel_conflict_whenReferenced() throws Exception {
9797
when(carModelRepo.findById(501L)).thenReturn(Optional.of(CarModelEntity.builder().id(501L).name("Avante").enabled(true).build()));
9898
when(pcmRepo.countByCarModelIdAndEnabledTrue(501L)).thenReturn(2L);
9999

100-
mockMvc.perform(delete("/api/v1/car-models/{id}", 501L))
100+
mockMvc.perform(patch("/api/v1/car-models/{id}/enable", 501L))
101101
.andExpect(status().isConflict())
102102
.andExpect(jsonPath("$.success", is(false)))
103103
.andExpect(jsonPath("$.status", is(ErrorStatus.CARMODEL_HAS_MAPPINGS.getStatusCode())))
104104
.andExpect(jsonPath("$.message", is(ErrorStatus.CARMODEL_HAS_MAPPINGS.getMessage())));
105105
}
106106

107107
@Test
108-
@DisplayName("DELETE /api/v1/car-models/{id} - 성공 시 enabled=false")
108+
@DisplayName("PATCH /api/v1/car-models/{id} - 성공 시 enabled=false")
109109
void deleteCarModel_success_soft() throws Exception {
110110
when(carModelRepo.findById(502L)).thenReturn(Optional.of(CarModelEntity.builder().id(502L).name("Sonata").enabled(true).build()));
111111
when(pcmRepo.countByCarModelIdAndEnabledTrue(502L)).thenReturn(0L);
112112

113-
mockMvc.perform(delete("/api/v1/car-models/{id}", 502L))
113+
mockMvc.perform(patch("/api/v1/car-models/{id}/enable", 502L))
114114
.andExpect(status().isOk())
115115
.andExpect(jsonPath("$.success", is(true)))
116-
.andExpect(jsonPath("$.status", is(SuccessStatus.SEND_CARMODEL_DELETE_SUCCESS.getStatusCode())))
117-
.andExpect(jsonPath("$.data.deleted", is(true)));
116+
.andExpect(jsonPath("$.status", is(SuccessStatus.SEND_CARMODEL_ENABLE_TOGGLED_SUCCESS.getStatusCode())))
117+
.andExpect(jsonPath("$.data.toggled", is(true)));
118118
}
119119
}

0 commit comments

Comments
 (0)