학습데이터 관리 목록 순서 변경, 주석추가
This commit is contained in:
@@ -19,6 +19,7 @@ import org.springframework.web.filter.OncePerRequestFilter;
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
|
||||
// JWT 필터를 타지 않게 할 URL 패턴들
|
||||
private static final String[] EXCLUDE_PATHS = {
|
||||
"/api/auth/signin", "/api/auth/refresh", "/api/auth/logout", "/api/members/*/password"
|
||||
};
|
||||
@@ -30,8 +31,10 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
// HTTP 요청 헤더에서 JWT 토큰 꺼내기
|
||||
String token = resolveToken(request);
|
||||
|
||||
// JWT 토큰을 검증하고, 인증된 사용자로 SecurityContext에 등록
|
||||
if (token != null && jwtTokenProvider.isValidToken(token)) {
|
||||
String username = jwtTokenProvider.getSubject(token);
|
||||
|
||||
@@ -57,7 +60,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
return false;
|
||||
}
|
||||
|
||||
// /api/members/{memberId}/password
|
||||
// HTTP 요청 헤더에서 JWT 토큰 꺼내기
|
||||
private String resolveToken(HttpServletRequest request) {
|
||||
String bearer = request.getHeader("Authorization");
|
||||
if (bearer != null && bearer.startsWith("Bearer ")) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import javax.crypto.SecretKey;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** 토큰 생성 */
|
||||
@Component
|
||||
public class JwtTokenProvider {
|
||||
|
||||
@@ -31,10 +32,12 @@ public class JwtTokenProvider {
|
||||
this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
// Access Token 생성
|
||||
public String createAccessToken(String subject) {
|
||||
return createToken(subject, accessTokenValidityInMs);
|
||||
}
|
||||
|
||||
// Refresh Token 생성
|
||||
public String createRefreshToken(String subject) {
|
||||
return createToken(subject, refreshTokenValidityInMs);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.core.ValueOperations;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/** redis token handler */
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RefreshTokenService {
|
||||
@@ -13,16 +14,33 @@ public class RefreshTokenService {
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
private static final String PREFIX = "RT:";
|
||||
|
||||
/**
|
||||
* Refresh Token 저장
|
||||
*
|
||||
* @param username 사용자 식별값 (보통 username or userId)
|
||||
* @param refreshToken 발급된 Refresh Token
|
||||
* @param ttlMillis 토큰 만료 시간 (밀리초 단위)
|
||||
*/
|
||||
public void save(String username, String refreshToken, long ttlMillis) {
|
||||
ValueOperations<String, String> ops = redisTemplate.opsForValue();
|
||||
ops.set(PREFIX + username, refreshToken, Duration.ofMillis(ttlMillis));
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh Token 검증
|
||||
*
|
||||
* <p>1. Redis에 저장된 값 조회 2. 클라이언트가 보낸 refreshToken과 비교 3. 동일하면 true
|
||||
*/
|
||||
public boolean validate(String username, String refreshToken) {
|
||||
String stored = redisTemplate.opsForValue().get(PREFIX + username);
|
||||
return stored != null && stored.equals(refreshToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh Token 삭제
|
||||
*
|
||||
* <p>로그아웃 시 호출 Redis에서 해당 사용자 토큰 제거
|
||||
*/
|
||||
public void delete(String username) {
|
||||
redisTemplate.delete(PREFIX + username);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,43 @@
|
||||
package com.kamco.cd.kamcoback.inference;
|
||||
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceResultShpDto;
|
||||
import com.kamco.cd.kamcoback.inference.service.InferenceManualService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "추론결과 데이터 생성", description = "추론결과 데이터 생성 API")
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/inference/manual")
|
||||
public class InferenceManualApiController {}
|
||||
public class InferenceManualApiController {
|
||||
|
||||
private final InferenceManualService inferenceManualService;
|
||||
|
||||
@Operation(summary = "추론 결과로 추론 목록 및 shp 생성", description = "추론 결과로 추론 목록 및 shp 생성")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "데이터 저장 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema =
|
||||
@Schema(implementation = InferenceResultShpDto.InferenceCntDto.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@PostMapping("/save")
|
||||
public void saveTesting(List<Long> batchIds) {
|
||||
inferenceManualService.saveResultsTesting(batchIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ public class InferenceResultShpApiController {
|
||||
@PostMapping("/shp/{uuid}")
|
||||
public ApiResponseDto<Void> createShp(
|
||||
@Parameter(example = "feb2ec0b-a0f7-49ca-95e4-98b2231bdaae") @PathVariable UUID uuid) {
|
||||
// shp 파일 수동생성
|
||||
inferenceResultShpService.createShp(uuid);
|
||||
return ApiResponseDto.createOK(null);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,61 @@
|
||||
package com.kamco.cd.kamcoback.inference.service;
|
||||
|
||||
import com.kamco.cd.kamcoback.common.exception.CustomApiException;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceResultsTestingDto;
|
||||
import com.kamco.cd.kamcoback.postgres.core.InferenceResultCoreService;
|
||||
import com.kamco.cd.kamcoback.postgres.core.ModelMngCoreService;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InferenceManualService {
|
||||
private final InferenceResultCoreService inferenceResultCoreService;
|
||||
private final ModelMngCoreService modelMngCoreService;
|
||||
|
||||
public void getResultsTesting(List<Long> batchIds) {
|
||||
List<InferenceResultsTestingDto.Basic> resultList =
|
||||
inferenceResultCoreService.getInferenceResults(batchIds);
|
||||
public void saveResultsTesting(List<Long> batchIds) {
|
||||
// 배치 id로 추론 결과 testing 테이블에서 조회
|
||||
List<InferenceResultsTestingDto.Basic> resultInfoList =
|
||||
inferenceResultCoreService.getInferenceResultGroupList(batchIds);
|
||||
|
||||
if (resultList.isEmpty()) {}
|
||||
if (resultInfoList.isEmpty()) {
|
||||
throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
// Long compareYear = resultInfoList.getFirst().getBeforeYear();
|
||||
// Long targetYear = resultInfoList.getFirst().getAfterYear();
|
||||
// String title = compareYear + "-" + targetYear + "변화탐지";
|
||||
//
|
||||
// InferenceResultDto.RegReq inferenceDto = new InferenceResultDto.RegReq();
|
||||
// inferenceDto.setTitle(title);
|
||||
// inferenceDto.setCompareYyyy(Integer.valueOf(compareYear));
|
||||
// inferenceDto.setTargetYyyy(targetYear);
|
||||
// // 추론 기본정보 저장
|
||||
// for (InferenceResultsTestingDto.Basic result : resultInfoList) {
|
||||
//
|
||||
// if (result.getModelVersion().startsWith(ModelType.G1.getId()) ||
|
||||
// result.getModelVersion().startsWith("M1")) {
|
||||
// ModelMngDto.Basic model =
|
||||
// modelMngCoreService.findByModelVer(result.getModelVersion());
|
||||
// inferenceDto.setModel1Uuid(model.getUuid());
|
||||
//
|
||||
// } else if (result.getModelVersion().startsWith("G2") ||
|
||||
// result.getModelVersion().startsWith("M2")) {
|
||||
// ModelMngDto.Basic model =
|
||||
// modelMngCoreService.findByModelVer(result.getModelVersion());
|
||||
// inferenceDto.setModel2Uuid(model.getUuid());
|
||||
//
|
||||
// } else if (result.getModelVersion().startsWith("G3") ||
|
||||
// result.getModelVersion().startsWith("M3")) {
|
||||
// ModelMngDto.Basic model =
|
||||
// modelMngCoreService.findByModelVer(result.getModelVersion());
|
||||
// inferenceDto.setModel3Uuid(model.getUuid());
|
||||
// }
|
||||
//
|
||||
// System.out.println(result);
|
||||
// }
|
||||
|
||||
for (InferenceResultsTestingDto.Basic result : resultList) {
|
||||
System.out.println(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ public class ModelMngDto {
|
||||
private String clsModelVersion;
|
||||
private Double priority;
|
||||
private String memo;
|
||||
private UUID uuid;
|
||||
|
||||
public Basic(
|
||||
Long modelUid,
|
||||
@@ -89,7 +90,8 @@ public class ModelMngDto {
|
||||
String clsModelFileName,
|
||||
String clsModelVersion,
|
||||
double priority,
|
||||
String memo) {
|
||||
String memo,
|
||||
UUID uuid) {
|
||||
this.modelUid = modelUid;
|
||||
this.modelVer = modelVer;
|
||||
this.createCompleteDttm = createCompleteDttm;
|
||||
@@ -111,6 +113,7 @@ public class ModelMngDto {
|
||||
this.clsModelVersion = clsModelVersion;
|
||||
this.priority = priority;
|
||||
this.memo = memo;
|
||||
this.uuid = uuid;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -575,11 +575,37 @@ public class InferenceResultCoreService {
|
||||
return mapSheetLearn5kRepository.getInferenceRunMapId(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 실패 처리되어야 할 목록 중에 아직 실패로 표시되지 않은 ID 조회
|
||||
*
|
||||
* @param uuid 추론 uuid
|
||||
* @param failMapIds AI API 연결하여 조회한 실패 job id
|
||||
* @param type 모델 타입
|
||||
* @return job id
|
||||
*/
|
||||
public List<Long> findFail5kList(UUID uuid, List<Long> failMapIds, String type) {
|
||||
return mapSheetLearn5kRepository.findFail5kList(uuid, failMapIds, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 완료된 것으로 들어온 목록 중 실제로 존재하는 5k jobId 조회
|
||||
*
|
||||
* @param uuid 추론 uuid
|
||||
* @param completedIds AI API 연결하여 조회한 성공 job id
|
||||
* @param type 모델 타입
|
||||
* @return job id
|
||||
*/
|
||||
public List<Long> findCompleted5kList(UUID uuid, List<Long> completedIds, String type) {
|
||||
return mapSheetLearn5kRepository.findCompleted5kList(uuid, completedIds, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* testing 테이블 결과로 기본정보 조회
|
||||
*
|
||||
* @param batchIds batch id
|
||||
* @return batch id, model ver, year 정보
|
||||
*/
|
||||
public List<InferenceResultsTestingDto.Basic> getInferenceResultGroupList(List<Long> batchIds) {
|
||||
return inferenceResultsTestingRepository.getInferenceResultGroupList(batchIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,4 +109,18 @@ public class ModelMngCoreService {
|
||||
.orElseThrow(() -> new EntityNotFoundException("모델 정보가 없습니다."));
|
||||
return entity.toDto();
|
||||
}
|
||||
|
||||
/**
|
||||
* 모델 버전명으로 조회
|
||||
*
|
||||
* @param ver 모델버전
|
||||
* @return 모델정보
|
||||
*/
|
||||
public ModelMngDto.Basic findByModelVer(String ver) {
|
||||
ModelMngEntity entity =
|
||||
modelMngRepository
|
||||
.findByModelVer(ver)
|
||||
.orElseThrow(() -> new EntityNotFoundException("모델 정보가 없습니다."));
|
||||
return entity.toDto();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ public class ModelMngEntity extends CommonDateEntity {
|
||||
this.clsModelFileName,
|
||||
this.clsModelVersion,
|
||||
this.priority,
|
||||
this.memo);
|
||||
this.memo,
|
||||
this.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
package com.kamco.cd.kamcoback.postgres.repository.Inference;
|
||||
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceResultsTestingDto;
|
||||
import com.kamco.cd.kamcoback.postgres.entity.InferenceResultsTestingEntity;
|
||||
import java.util.List;
|
||||
|
||||
public interface InferenceResultsTestingRepositoryCustom {
|
||||
|
||||
/**
|
||||
* 추론 결과 조회
|
||||
*
|
||||
* @param batchIds batch id
|
||||
* @return 추론 결과 목록
|
||||
*/
|
||||
List<InferenceResultsTestingEntity> getInferenceResultList(List<Long> batchIds);
|
||||
|
||||
/**
|
||||
@@ -14,4 +21,12 @@ public interface InferenceResultsTestingRepositoryCustom {
|
||||
* @return batchIds 조회 count 수
|
||||
*/
|
||||
Long getInferenceResultCnt(List<Long> batchIds);
|
||||
|
||||
/**
|
||||
* testing 테이블 결과로 기본정보 조회
|
||||
*
|
||||
* @param batchIds batch id
|
||||
* @return batch id, model ver, year 정보
|
||||
*/
|
||||
List<InferenceResultsTestingDto.Basic> getInferenceResultGroupList(List<Long> batchIds);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package com.kamco.cd.kamcoback.postgres.repository.Inference;
|
||||
|
||||
import static com.kamco.cd.kamcoback.postgres.entity.QInferenceResultsTestingEntity.inferenceResultsTestingEntity;
|
||||
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceResultsTestingDto;
|
||||
import com.kamco.cd.kamcoback.postgres.entity.InferenceResultsTestingEntity;
|
||||
import com.querydsl.core.types.Projections;
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -47,4 +49,29 @@ public class InferenceResultsTestingRepositoryImpl
|
||||
|
||||
return cnt == null ? 0L : cnt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<InferenceResultsTestingDto.Basic> getInferenceResultGroupList(List<Long> batchIds) {
|
||||
return queryFactory
|
||||
.select(
|
||||
Projections.constructor(
|
||||
InferenceResultsTestingDto.Basic.class,
|
||||
inferenceResultsTestingEntity.batchId,
|
||||
inferenceResultsTestingEntity.modelVersion.max(),
|
||||
inferenceResultsTestingEntity.beforeYear.max(),
|
||||
inferenceResultsTestingEntity.afterYear.max()))
|
||||
.from(inferenceResultsTestingEntity)
|
||||
.where(
|
||||
inferenceResultsTestingEntity
|
||||
.batchId
|
||||
.in(batchIds)
|
||||
.and(inferenceResultsTestingEntity.afterC.isNotNull())
|
||||
.and(inferenceResultsTestingEntity.afterP.isNotNull()))
|
||||
.groupBy(
|
||||
inferenceResultsTestingEntity.batchId,
|
||||
inferenceResultsTestingEntity.modelVersion,
|
||||
inferenceResultsTestingEntity.beforeYear,
|
||||
inferenceResultsTestingEntity.afterYear)
|
||||
.fetch();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,42 @@ import java.util.UUID;
|
||||
|
||||
public interface MapSheetLearn5kRepositoryCustom {
|
||||
|
||||
/**
|
||||
* 추론 실행 실패 정보 저장
|
||||
*
|
||||
* @param uuid 추론 uuid
|
||||
* @param jobDto AI API에서 조회한 Job 정보
|
||||
* @param type 모델 타입
|
||||
*/
|
||||
void saveFail5k(UUID uuid, JobStatusDto jobDto, String type);
|
||||
|
||||
/**
|
||||
* 추론 실행중인 Job id 저장
|
||||
*
|
||||
* @param uuid 추론 uuid
|
||||
* @param jobDto AI API에서 조회한 Job 정보
|
||||
* @param type 모델 타입
|
||||
*/
|
||||
void saveJobId(UUID uuid, JobStatusDto jobDto, String type);
|
||||
|
||||
/**
|
||||
* 실패 처리되어야 할 목록 중에 아직 실패로 표시되지 않은 ID 조회
|
||||
*
|
||||
* @param uuid 추론 uuid
|
||||
* @param failIds AI API 연결하여 조회한 실패 job id
|
||||
* @param type 모델 타입
|
||||
* @return 실패로 표시되지 않은 ID
|
||||
*/
|
||||
List<Long> findFail5kList(UUID uuid, List<Long> failIds, String type);
|
||||
|
||||
/**
|
||||
* 완료된 것으로 들어온 목록 중 실제로 존재하는 5k jobId 조회
|
||||
*
|
||||
* @param uuid 추론 uuid
|
||||
* @param completedIds AI API 연결하여 조회한 실패 job id
|
||||
* @param type 모델 타입
|
||||
* @return 성공한 job id
|
||||
*/
|
||||
List<Long> findCompleted5kList(UUID uuid, List<Long> completedIds, String type);
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetLearnEntity.mapShe
|
||||
|
||||
import com.kamco.cd.kamcoback.common.enums.ImageryFitStatus;
|
||||
import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.InspectState;
|
||||
import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelMngState;
|
||||
import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelState;
|
||||
import com.kamco.cd.kamcoback.label.dto.LabelWorkDto;
|
||||
import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.LabelWorkMng;
|
||||
@@ -166,6 +167,19 @@ public class LabelWorkRepositoryImpl implements LabelWorkRepositoryCustom {
|
||||
.from(labelingAssignmentEntity)
|
||||
.where(labelingAssignmentEntity.analUid.eq(mapSheetAnalInferenceEntity.id));
|
||||
|
||||
/** 순서 ING 진행중 ASSIGNED 작업할당 PENDING 작업대기 FINISH 종료 */
|
||||
NumberExpression<Integer> stateOrder =
|
||||
new CaseBuilder()
|
||||
.when(mapSheetAnalInferenceEntity.analState.eq(LabelMngState.ING.getId()))
|
||||
.then(0)
|
||||
.when(mapSheetAnalInferenceEntity.analState.eq(LabelMngState.ASSIGNED.getId()))
|
||||
.then(1)
|
||||
.when(mapSheetAnalInferenceEntity.analState.eq(LabelMngState.PENDING.getId()))
|
||||
.then(2)
|
||||
.when(mapSheetAnalInferenceEntity.analState.eq(LabelMngState.FINISH.getId()))
|
||||
.then(3)
|
||||
.otherwise(99);
|
||||
|
||||
List<LabelWorkMng> foundContent =
|
||||
queryFactory
|
||||
.select(
|
||||
@@ -251,6 +265,7 @@ public class LabelWorkRepositoryImpl implements LabelWorkRepositoryCustom {
|
||||
mapSheetLearnEntity.uid,
|
||||
mapSheetLearnEntity.uuid)
|
||||
.orderBy(
|
||||
stateOrder.asc(),
|
||||
mapSheetAnalInferenceEntity.targetYyyy.desc(),
|
||||
mapSheetAnalInferenceEntity.compareYyyy.desc(),
|
||||
mapSheetAnalInferenceEntity.stage.desc())
|
||||
|
||||
@@ -39,4 +39,12 @@ public interface ModelMngRepositoryCustom {
|
||||
Optional<ModelMngEntity> findByModelId(Long id);
|
||||
|
||||
Optional<ModelMngEntity> findByModelId(UUID id);
|
||||
|
||||
/**
|
||||
* 버전명으로 모델 조회
|
||||
*
|
||||
* @param ver 모델버전
|
||||
* @return 모델정보
|
||||
*/
|
||||
Optional<ModelMngEntity> findByModelVer(String ver);
|
||||
}
|
||||
|
||||
@@ -246,4 +246,14 @@ public class ModelMngRepositoryImpl extends QuerydslRepositorySupport
|
||||
return Optional.ofNullable(
|
||||
queryFactory.selectFrom(modelMngEntity).where(modelMngEntity.uuid.eq(uuid)).fetchOne());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ModelMngEntity> findByModelVer(String ver) {
|
||||
return Optional.ofNullable(
|
||||
queryFactory
|
||||
.selectFrom(modelMngEntity)
|
||||
.where(modelMngEntity.modelVer.eq(ver))
|
||||
.limit(1)
|
||||
.fetchOne());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,6 +423,7 @@ public class MapSheetInferenceJobService {
|
||||
.map(Long::valueOf)
|
||||
.toList();
|
||||
|
||||
// 추론 실행 도엽별 정보 조회
|
||||
List<Long> jobIds = inferenceResultCoreService.findFail5kList(uuid, failedIds, type);
|
||||
|
||||
Set<Long> jobIdSet = new HashSet<>(jobIds);
|
||||
|
||||
Reference in New Issue
Block a user