봄 MVC 패치 방식: 부분 업데이트
Spring MVC + Jackson을 사용하여 REST 서비스를 구축하는 프로젝트가 있습니다.다음과 같은 Java 엔티티가 있다고 가정합니다.
public class MyEntity {
private Integer id;
private boolean aBoolean;
private String aVeryBigString;
//getter & setters
}
부울 값만 업데이트하고 싶을 때가 있는데, 단순히 부울 값을 업데이트하는 것만으로 개체 전체를 큰 문자열과 함께 보내는 것은 좋지 않다고 생각합니다.따라서 PATCH HTTP 방식을 사용하여 갱신해야 할 필드만 전송할 것을 검토했습니다.따라서 컨트롤러에서 다음 방법을 선언합니다.
@RequestMapping(method = RequestMethod.PATCH)
public void patch(@RequestBody MyVariable myVariable) {
//calling a service to update the entity
}
문제는 어떤 필드를 갱신해야 하는지 어떻게 알 수 있는가 하는 것입니다.예를 들어 클라이언트가 부울만 업데이트하려는 경우 빈 "aVeryBigString"을 가진 개체를 가져옵니다.사용자가 부울만 업데이트하고 문자열을 비우지 않는 것을 어떻게 알 수 있습니까?
커스텀 URL을 구축하여 문제를 해결했습니다.예를 들어 /myentities/1/aboolean/true URL은 부울만 갱신할 수 있는 메서드에 매핑됩니다.이 솔루션의 문제는 REST에 준거하지 않는다는 것입니다.100% REST에 준거하고 싶지는 않지만, 각 필드를 갱신하기 위한 커스텀 URL을 제공하는 것은 불편합니다(특히 여러 필드를 갱신하고 싶을 때 문제가 발생합니다).
또 다른 솔루션은 "MyEntity"를 여러 리소스로 분할하여 이러한 리소스를 업데이트하는 것입니다. 그러나 "MyEntity"는 일반 리소스이며 다른 리소스로 구성되어 있지 않습니다.
그렇다면, 이 문제를 해결할 수 있는 우아한 방법은 없을까?
늦어질 수도 있지만 신입사원이나 같은 문제를 겪고 있는 분들을 위해 나만의 해결책을 알려드리겠습니다.
이전 프로젝트에서는 간단하게 말하면 네이티브 Java Map을 사용하고 있습니다.클라이언트가 명시적으로 null로 설정한 null 값을 포함한 모든 새 값을 캡처합니다.이 시점에서 도메인모델과 같은 POJO를 사용하는 경우와 달리 어떤 Java 속성을 null로 설정해야 하는지 쉽게 판단할 수 있습니다.또, 클라이언트에 의해서 어떤 필드가 null로 설정되어 있는지, 또 어떤 필드가 업데이트에 포함되어 있지 않지만, 디폴트로는 null이 됩니다.
또한 업데이트하려는 레코드의 ID를 전송하기 위해 http 요구를 요구해야 하며 패치 데이터 구조에 포함시키지 마십시오.URL 내의 ID를 경로변수로 설정하고 패치데이터를 패치바디로 설정합니다.그런 다음 ID를 사용하여 먼저 도메인 모델을 통해 레코드를 얻은 다음 HashMap을 사용하여 매퍼 서비스 또는 유틸리티를 사용하여 해당 도메인 모델에 대한 변경 사항을 패치할 수 있습니다.
갱신하다
이러한 종류의 범용 코드를 사용하여 서비스에 대한 추상 슈퍼 클래스를 만들 수 있습니다. Java Generics를 사용해야 합니다.이것은 가능한 구현의 일부일 뿐이므로 이해하시기 바랍니다.또한 Orika나 Dozer와 같은 Mapper 프레임워크를 사용하는 것이 좋습니다.
public abstract class AbstractService<Entity extends BaseEntity, DTO extends BaseDto> {
@Autowired
private MapperService mapper;
@Autowired
private BaseRepo<Entity> repo;
private Class<DTO> dtoClass;
private Class<Entity> entityCLass;
public AbstractService(){
entityCLass = (Class<Entity>) SomeReflectionTool.getGenericParameter()[0];
dtoClass = (Class<DTO>) SomeReflectionTool.getGenericParameter()[1];
}
public DTO patch(Long id, Map<String, Object> patchValues) {
Entity entity = repo.get(id);
DTO dto = mapper.map(entity, dtoClass);
mapper.map(patchValues, dto);
Entity updatedEntity = toEntity(dto);
save(updatedEntity);
return dto;
}
}
올바른 방법은 JSON PATCH RFC 6902에서 제안된 방법입니다.
요청의 예는 다음과 같습니다.
PATCH http://example.com/api/entity/1 HTTP/1.1
Content-Type: application/json-patch+json
[
{ "op": "replace", "path": "aBoolean", "value": true }
]
해 본 , MVC에서 되고 있는 것과 하고 있는 을 알 수 .DomainObjectReader 항목:JsonPatchHandler
import org.springframework.data.rest.webmvc.mapping.Associations
@RepositoryRestController
public class BookCustomRepository {
private final DomainObjectReader domainObjectReader;
private final ObjectMapper mapper;
private final BookRepository repository;
@Autowired
public BookCustomRepository(BookRepository bookRepository,
ObjectMapper mapper,
PersistentEntities persistentEntities,
Associations associationLinks) {
this.repository = bookRepository;
this.mapper = mapper;
this.domainObjectReader = new DomainObjectReader(persistentEntities, associationLinks);
}
@PatchMapping(value = "/book/{id}", consumes = {MediaType.APPLICATION_JSON_UTF8_VALUE, MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<?> patch(@PathVariable String id, ServletServerHttpRequest request) throws IOException {
Book entityToPatch = repository.findById(id).orElseThrow(ResourceNotFoundException::new);
Book patched = domainObjectReader.read(request.getBody(), entityToPatch, mapper);
repository.save(patched);
return ResponseEntity.noContent().build();
}
}
PATCH엔티티 표현 전체를 송신하지 않기 때문에 빈 문자열에 대한 코멘트를 이해할 수 없습니다.다음과 같은 간단한 JSON을 처리해야 합니다.
{ aBoolean: true }
지정된 리소스에 적용합니다.수신된 것은 원하는 자원 상태와 현재 자원 상태의 차이입니다.
은 사용 가능/사용 가능/사용할 수 없다/사용할 수 없다/사용할 수 없다PATCH이미 발생한 동일한 문제로 인해 개체에 패치를 적용합니다.JSON 디세리얼라이저 Java POJO 。
엔티티에 를 적용하기 엔티티에 패치를 적용하기 위한 논리를 사용하는 만).PATCH 아니다POST를 참조해 주세요.
하는지, 빈 문자열은 " " "입니다.")을 사용합니다.null(어느쪽이든)또는 오버라이드된 값을 정의하는 추가 파라미터를 지정해야 합니다.마지막은 문제 없습니다.JavaScript 응용 프로그램은 서버에 나열되는 JSON 본문과 함께 변경 및 전송된 필드를 인식합니다. 필드 " " " " 입니다.description변경(패치)하도록 이름이 지정되었지만 JSON 본문에 지정되지 않았습니다.그것은 무효가 되어 있었습니다.
하면 .Optional<>경우 : 우 for :
public class MyEntityUpdate {
private Optional<String> aVeryBigString;
}
이렇게 하면 다음과 같이 업데이트 개체를 검사할 수 있습니다.
if(update.getAVeryBigString() != null)
entity.setAVeryBigString(update.getAVeryBigString().get());
[ ] [ If ] 。aVeryBigString 문서, JSON 문서, POJO JSON 문서, POJO 문서에는 .aVeryBigString는 다음과 같습니다.null문서에 이 있는 경우null는 [POJO]가 .Optional값어치가 있는null이 솔루션을 사용하면 "업데이트 없음"과 "늘로 설정"을 구분할 수 있습니다.
제공된 답변의 대부분은 모두 JSON 패치 적용 또는 불완전한 답변입니다.아래는 실제 코드의 기능에 필요한 모든 설명과 예시입니다.
풀 패치 기능:
@ApiOperation(value = "Patch an existing claim with partial update")
@RequestMapping(value = CLAIMS_V1 + "/{claimId}", method = RequestMethod.PATCH)
ResponseEntity<Claim> patchClaim(@PathVariable Long claimId, @RequestBody Map<String, Object> fields) {
// Sanitize and validate the data
if (claimId <= 0 || fields == null || fields.isEmpty() || !fields.get("claimId").equals(claimId)){
return new ResponseEntity<>(HttpStatus.BAD_REQUEST); // 400 Invalid claim object received or invalid id or id does not match object
}
Claim claim = claimService.get(claimId);
// Does the object exist?
if( claim == null){
return new ResponseEntity<>(HttpStatus.NOT_FOUND); // 404 Claim object does not exist
}
// Remove id from request, we don't ever want to change the id.
// This is not necessary,
// loop used below since we checked the id above
fields.remove("claimId");
fields.forEach((k, v) -> {
// use reflection to get field k on object and set it to value v
// Change Claim.class to whatver your object is: Object.class
Field field = ReflectionUtils.findField(Claim.class, k); // find field in the object class
field.setAccessible(true);
ReflectionUtils.setField(field, claim, v); // set given field for defined object to value V
});
claimService.saveOrUpdate(claim);
return new ResponseEntity<>(claim, HttpStatus.OK);
}
새로운 개발자들은 보통 그런 반사를 다루지 않기 때문에 위의 내용은 일부 사람들에게는 혼란스러울 수 있습니다.기본적으로 본문에서 이 함수를 전달하더라도 지정된 ID를 사용하여 관련 클레임을 찾은 다음 키 값 쌍으로 전달한 필드만 업데이트합니다.
본문 예:
패치 / 청구 / 7
{
"claimId":7,
"claimTypeId": 1,
"claimStatus": null
}
위는 클레임을 갱신합니다.Claim 7에 대해 지정된 값으로 Id 및 claimStatus를 입력하고 다른 모든 값은 변경되지 않습니다.
따라서 다음과 같은 결과를 얻을 수 있습니다.
{
"claimId": 7,
"claimSrcAcctId": 12345678,
"claimTypeId": 1,
"claimDescription": "The vehicle is damaged beyond repair",
"claimDateSubmitted": "2019-01-11 17:43:43",
"claimStatus": null,
"claimDateUpdated": "2019-04-09 13:43:07",
"claimAcctAddress": "123 Sesame St, Charlotte, NC 28282",
"claimContactName": "Steve Smith",
"claimContactPhone": "777-555-1111",
"claimContactEmail": "steve.smith@domain.com",
"claimWitness": true,
"claimWitnessFirstName": "Stan",
"claimWitnessLastName": "Smith",
"claimWitnessPhone": "777-777-7777",
"claimDate": "2019-01-11 17:43:43",
"claimDateEnd": "2019-01-11 12:43:43",
"claimInvestigation": null,
"scoring": null
}
보시다시피 변경할 데이터 이외의 데이터는 변경하지 않고 전체 개체가 다시 표시됩니다.여기 설명에 약간의 반복이 있는 것을 알고 있습니다.그냥 개요를 명확하게 하고 싶었어요.
서비스를 변경할 수 없기 때문에 이렇게 문제를 해결했습니다.
public class Test {
void updatePerson(Person person,PersonPatch patch) {
for (PersonPatch.PersonPatchField updatedField : patch.updatedFields) {
switch (updatedField){
case firstname:
person.setFirstname(patch.getFirstname());
continue;
case lastname:
person.setLastname(patch.getLastname());
continue;
case title:
person.setTitle(patch.getTitle());
continue;
}
}
}
public static class PersonPatch {
private final List<PersonPatchField> updatedFields = new ArrayList<PersonPatchField>();
public List<PersonPatchField> updatedFields() {
return updatedFields;
}
public enum PersonPatchField {
firstname,
lastname,
title
}
private String firstname;
private String lastname;
private String title;
public String getFirstname() {
return firstname;
}
public void setFirstname(final String firstname) {
updatedFields.add(PersonPatchField.firstname);
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(final String lastname) {
updatedFields.add(PersonPatchField.lastname);
this.lastname = lastname;
}
public String getTitle() {
return title;
}
public void setTitle(final String title) {
updatedFields.add(PersonPatchField.title);
this.title = title;
}
}
Jackson은 값이 존재할 때만 호출했습니다.호출된 세터를 저장할 수 있습니다.
- MapStruct를 통한 솔루션
@Mapper(componentModel = "spring")
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface CustomerMapper {
void updateCustomerFromDto(CustomerDto dto, @MappingTarget Customer entity);
}
public void updateCustomer(CustomerDto dto) {
Customer myCustomer = repo.findById(dto.id);
mapper.updateCustomerFromDto(dto, myCustomer);
repo.save(myCustomer);
}
이 접근법의 단점은 업데이트 중에 null 값을 데이터베이스에 전달할 수 없다는 것입니다.
스프링 데이터를 사용한 부분 데이터 업데이트 참조
- json-patch 라이브러리를 통한 솔루션
- 스프링 데이터 레스트를 통한 솔루션
Spring Data Rest 기능이 있는 Custom Spring MVC HTTP Patch Requests를 참조하십시오.
업데이트된 필드로 구성된 객체를 보내주시면 안 될까요?
스크립트 호출:
var data = JSON.stringify({
aBoolean: true
});
$.ajax({
type: 'patch',
contentType: 'application/json-patch+json',
url: '/myentities/' + entity.id,
data: data
});
스프링 MVC 컨트롤러:
@PatchMapping(value = "/{id}")
public ResponseEntity<?> patch(@RequestBody Map<String, Object> updates, @PathVariable("id") String id)
{
// updates now only contains keys for fields that was updated
return ResponseEntity.ok("resource updated");
}
「」내path "", "/"의 합니다.updates를 참조해 주세요."aBoolean"는 값을 합니다.true다음 단계는 엔티티 설정자를 호출하여 값을 실제로 할당하는 것입니다.하지만, 그것은 다른 종류의 문제입니다.
나는 이 문제를 해결하기 위해 반성을 이용한다.클라이언트는 오브젝트(예를 들어 javascript)를 송신할 수 있습니다.이 오브젝트에는, 존중되는 값을 가지는 모든 필드가 포함됩니다.컨트롤러에서 새로운 값을 캡처하는 방법:
@PatchMapping(value = "{id}")
public HttpEntity<Map<String, Object>> updatePartial(@PathVariable Integer id, @RequestBody Map<String, Object> data) {
return ResponseEntity.ok(questionService.updatePartial(id, data));
}
그런 다음 서비스 구현에 반영을 사용하여 요청된 속성이 존재하는지, 존재하는지 여부를 확인한 후 값을 업데이트할 수 있습니다.
public Map<String, Object> updatePartial(@NotNull Long id, @NotNull Map<String, Object> data) {
Post post = postRepository.findById(id);
Field[] postFields = Post.class.getDeclaredFields();
HashMap<String, Object> toReturn = new HashMap<>(1);
for (Field postField : postFields) {
data.forEach((key, value) -> {
if (key.equalsIgnoreCase(postField.getName())) {
try {
final Field declaredField = Post.class.getDeclaredField(key);
declaredField.setAccessible(true);
declaredField.set(post, value);
toReturn.put(key, value);
} catch (NoSuchFieldException | IllegalAccessException e) {
log.error("Unable to do partial update field: " + key + " :: ", e);
throw new BadRequestException("Something went wrong at server while partial updation");
}
}
});
}
postRepository.save(post);
return toReturn;
}
여기서 Spring Data JPA는 DB 작업에 사용됩니다.
★★★★★★★★★★★★★★★★★★★★에서 어떻게 대처해야 하는지 알고 싶다면. PATCH다음과 같은 데이터를 가진 모든 엔드포인트에 호출합니다.
{
voted: true,
reported: true
}
그런 다음 응답에서 클라이언트는 응답에 예상 속성이 포함되어 있는지 확인할 수 있습니다.예: 모든 필드를 예상하고 있습니다(에서 패러머로 전달했습니다).PATCH의 응답 )의 응답:
if (response.data.hasOwnProperty("voted")){
//do Something
} else{
//do something e.g report it
}
다음은 googles GSON을 사용한 패치명령어의 실장을 나타냅니다.
package de.tef.service.payment;
import com.google.gson.*;
class JsonHelper {
static <T> T patch(T object, String patch, Class<T> clazz) {
JsonElement o = new Gson().toJsonTree(object);
JsonObject p = new JsonParser().parse(patch).getAsJsonObject();
JsonElement result = patch(o, p);
return new Gson().fromJson(result, clazz);
}
static JsonElement patch(JsonElement object, JsonElement patch) {
if (patch.isJsonArray()) {
JsonArray result = new JsonArray();
object.getAsJsonArray().forEach(result::add);
return result;
} else if (patch.isJsonObject()) {
System.out.println(object + " => " + patch);
JsonObject o = object.getAsJsonObject();
JsonObject p = patch.getAsJsonObject();
JsonObject result = new JsonObject();
o.getAsJsonObject().entrySet().stream().forEach(e -> result.add(e.getKey(), p.get(e.getKey()) == null ? e.getValue() : patch(e.getValue(), p.get(e.getKey()))));
return result;
} else if (patch.isJsonPrimitive()) {
return patch;
} else if (patch.isJsonNull()) {
return patch;
} else {
throw new IllegalStateException();
}
}
}
구현은 중첩된 구조에 주의를 기울이기 위해 반복됩니다.어레이에는 병합 키가 없기 때문에 어레이는 병합되지 않습니다.
"패치" JSON은 NULL로 채워지지 않은 필드를 NULL로 채우는 필드와 구분하기 위해 String에서 JsonElement로 직접 변환되지 않습니다.
이것은 오래된 포스트이지만, 나에게 좋은 해결책이 없는 것은 여전히 문제였습니다.이것이 내가 지향하는 바이다.
이 개념은 역직렬화 단계를 활용하여 무엇이 전송되고 무엇이 전송되지 않는지 추적하고 기업이 속성 변경 상태를 조사하는 방법을 지원하도록 하는 것입니다.아이디어는 이렇습니다.
이 인터페이스는 사용자 지정 역직렬화를 트리거하고 콩이 강제로 상태 변경 정보를 전송하도록 합니다.
@JsonDeserialize(using = Deser.class)
interface Changes {
default boolean changed(String name) {
Set<String> changed = changes();
return changed != null && changed.contains(name);
}
void changes(Set<String> changed);
Set<String> changes();
}
여기 탈세련기 있어요.한번 호출되면 믹스인을 통해 역직렬화 동작을 되돌립니다.json 속성이 bean 속성에 직접 매핑될 때만 작동합니다.좀 더 화려한 건 빈 인스턴스가 프록시가 되고 셋터 콜이 감청될 수 있을 것 같아요
class Deser extends JsonDeserializer<Object> implements ContextualDeserializer {
private Class<?> targetClass;
public Deser() {}
public Deser(Class<?> targetClass) { this.targetClass = targetClass; }
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
ObjectMapper mapper = (ObjectMapper) p.getCodec();
TypeReference<HashMap<String, Object>> typeRef = new TypeReference<>() {
};
HashMap<String, Object> map = p.readValueAs(typeRef);
ObjectMapper innerMapper = mapper.copy();
innerMapper.addMixIn(targetClass, RevertDefaultDeserialize.class);
Object o = innerMapper.convertValue(map, targetClass);
// this will only work with simple json->bean property mapping
((Changes) o).changes(map.keySet());
return o;
}
@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
Class<?> targetClass = ctxt.getContextualType().getRawClass();
return new Deser(targetClass);
}
@JsonDeserialize
interface RevertDefaultDeserialize {
}
}
질문에서 나온 콩의 모양은 이렇습니다.컨트롤러 인터페이스에서 사용되는 데이터 전송 빈과 JPA 엔티티를 분할합니다만, 이 빈은 같은 빈입니다.
상속이 가능한 경우 기본 클래스에서 변경을 지원할 수 있지만 여기서는 인터페이스 자체가 직접 사용됩니다.
@Data
class MyEntity implements Changes {
private Integer id;
private boolean aBoolean;
private String aVeryBigString;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private Set<String> changes;
@Override
public void changes(Set<String> changed) {
this.changes = changed;
}
@Override
public Set<String> changes() {
return changes;
}
}
그리고 여기 사용법이 있습니다.
class HowToUseIt {
public static void example(MyEntity bean) {
if (bean.changed("id")) {
Integer id = bean.getId();
// ...
}
if (bean.changed("aBoolean")) {
boolean aBoolean = bean.isABoolean();
// ...
}
if (bean.changed("aVeryBigString")) {
String aVeryBigString = bean.getAVeryBigString();
// ...
}
}
}
JpaRepository를 구현하는 경우 이 기능을 사용할 수 있습니다.
@Modifying
@Query("update Customer u set u.phone = :phone where u.id = :id")
void updatePhone(@Param(value = "id") long id, @Param(value = "phone") String phone);
이 밖에도 훌륭한 접근법이 많이 있습니다만, 아직 언급되지 않았기 때문에 추가하려고 합니다.요구에 따라 갱신된 필드 목록을 추가하지 않고도 늘 수 있는 필드를 처리할 수 있다는 장점이 있습니다.이 접근법에는 다음과 같은 특성이 있습니다.
- 요청으로 전송된 필드만 업데이트됩니다.
- 누락된 필드는 무시됩니다.
- 명시적으로 송신된 필드
null에서 JSON으로 됩니다.null
따라서 다음 도메인 개체가 지정됩니다.
public class User {
Integer id;
String firstName;
String lastName;
}
사용자를 증분 업데이트하기 위한 컨트롤러 방식은 다음과 같습니다.이 방식은 제네릭을 사용하여 도메인 개체에 적합한 정적 방식으로 쉽게 추출할 수 있습니다.
public class UserController {
@Autowired ObjectMapper om;
@Autowired
@Qualifier("mvcValidator")
private Validator validator;
// assume this is a JPARepository
@Autowired
private UserRepository userRepo;
@PostMapping(value = "/{userId}", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> incrementalUpdate(@PathVariable("userId") Integer userId,
@RequestBody requestJson) {
final User existingUser = this.userRepo.findById(userId).orElse(null);
if(existingUser == null) {
return ResponseEntity.notFound().build();
}
// OPTIONAL - validate the request, since we can't use @Validated
try {
final User incomingUpdate = om.readValue(updateJson, User.class);
final BeanPropertyBindingResult validationResult
= new BeanPropertyBindingResult(incomingUpdate, "user");
this.validator.validate(incomingUpdate, validationResult);
if (validationResult.hasErrors()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
} catch (JsonProcessingException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
// merge the incoming update into the existing user
try {
this.om.readerForUpdating(existingUser).readValue(updateJson, User.class);
} catch(IOException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
this.userRepo.save(existingUser);
return ResponseEntity.noContent().build();
}
}
또는, 오브젝트 또는 컬렉션은 ""로 을 붙여야 .@JsonMerge그 이외의 경우는, 재귀적으로 Marge 하는 대신에, 착신치에 의해서 무조건 덮어쓰기 됩니다.
제 답변이 늦어질 수도 있지만 같은 문제를 겪고 있는 사람들이 아직 있다면요.가능한 모든 솔루션을 PATCH와 함께 사용했지만 오브젝트의 필드를 업데이트하지 못했습니다.그래서 POST로 전환하여 POST를 통해 변경되지 않은 필드의 값을 변경하지 않고 특정 필드를 업데이트할 수 있습니다.
부울을 부울로 변경하고 업데이트하지 않을 모든 필드에 null 값을 할당할 수 있습니다.null 값이 아닌 유일한 값은 업데이트할 필드 클라이언트를 정의합니다.
언급URL : https://stackoverflow.com/questions/17860520/spring-mvc-patch-method-partial-updates
'programing' 카테고리의 다른 글
| 의 목적은 무엇입니까?Angular 6에서 서비스를 생성할 때 주입식 장식기와 함께? (0) | 2023.03.06 |
|---|---|
| 커스텀 Marshal JSON()이 이동 중에 호출되지 않음 (0) | 2023.03.06 |
| Newtonsoft 개체 → JSON 문자열 가져오기 (0) | 2023.03.06 |
| TypeScript 또는 JavaScript 유형 캐스팅 (0) | 2023.03.06 |
| Wordpress - 메뉴 항목에 하위 항목이 있는지 어떻게 알 수 있습니까? (0) | 2023.03.06 |