Merge branch 'feat/infer_dev_260107' of https://kamco.gitea.gs.dabeeo.com/dabeeo/kamco-dabeeo-backoffice into feat/infer_dev_260107
# Conflicts: # src/main/resources/application-local.yml
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
package com.kamco.cd.kamcoback.postgres.core;
|
||||
|
||||
import com.kamco.cd.kamcoback.gukyuin.dto.GukYuinDto.GukYuinLinkFacts;
|
||||
import com.kamco.cd.kamcoback.postgres.repository.Inference.MapSheetLearnRepository;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GukYuinCoreService {
|
||||
|
||||
private final MapSheetLearnRepository mapSheetLearnRepository;
|
||||
|
||||
/**
|
||||
* 국유in연동 가능여부 확인
|
||||
*
|
||||
* @param uuid uuid
|
||||
* @return
|
||||
*/
|
||||
public GukYuinLinkFacts findLinkFacts(UUID uuid) {
|
||||
return mapSheetLearnRepository.findLinkFacts(uuid);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.kamco.cd.kamcoback.common.exception.CustomApiException;
|
||||
import com.kamco.cd.kamcoback.common.utils.UserUtil;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.AnalResultInfo;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.BboxPointDto;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.Dashboard;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.Geom;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.InferenceBatchSheet;
|
||||
@@ -409,8 +410,18 @@ public class InferenceResultCoreService {
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 추론결과 기본정보
|
||||
*
|
||||
* @param uuid uuid
|
||||
* @return
|
||||
*/
|
||||
public AnalResultInfo getInferenceResultInfo(UUID uuid) {
|
||||
return mapSheetLearnRepository.getInferenceResultInfo(uuid);
|
||||
AnalResultInfo resultInfo = mapSheetLearnRepository.getInferenceResultInfo(uuid);
|
||||
BboxPointDto bboxPointDto = mapSheetLearnRepository.getBboxPoint(uuid);
|
||||
resultInfo.setBboxGeom(bboxPointDto.getBboxGeom());
|
||||
resultInfo.setBboxCenterPoint(bboxPointDto.getBboxCenterPoint());
|
||||
return resultInfo;
|
||||
}
|
||||
|
||||
public List<Dashboard> getInferenceClassCountList(UUID uuid) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.util.UUID;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -51,6 +52,12 @@ public class AuditLogEntity extends CommonCreateEntity {
|
||||
@Column(name = "error_log_uid")
|
||||
private Long errorLogUid;
|
||||
|
||||
@Column(name = "download_uuid")
|
||||
private UUID downloadUuid;
|
||||
|
||||
@Column(name = "login_attempt_id")
|
||||
private String loginAttemptId;
|
||||
|
||||
public AuditLogEntity(
|
||||
Long userUid,
|
||||
EventType eventType,
|
||||
@@ -59,7 +66,9 @@ public class AuditLogEntity extends CommonCreateEntity {
|
||||
String ipAddress,
|
||||
String requestUri,
|
||||
String requestBody,
|
||||
Long errorLogUid) {
|
||||
Long errorLogUid,
|
||||
UUID downloadUuid,
|
||||
String loginAttemptId) {
|
||||
this.userUid = userUid;
|
||||
this.eventType = eventType;
|
||||
this.eventStatus = eventStatus;
|
||||
@@ -68,11 +77,18 @@ public class AuditLogEntity extends CommonCreateEntity {
|
||||
this.requestUri = requestUri;
|
||||
this.requestBody = requestBody;
|
||||
this.errorLogUid = errorLogUid;
|
||||
this.downloadUuid = downloadUuid;
|
||||
this.loginAttemptId = loginAttemptId;
|
||||
}
|
||||
|
||||
/** 파일 다운로드 이력 생성 */
|
||||
public static AuditLogEntity forFileDownload(
|
||||
Long userId, String requestUri, String menuUid, String ip, int httpStatus) {
|
||||
Long userId,
|
||||
String requestUri,
|
||||
String menuUid,
|
||||
String ip,
|
||||
int httpStatus,
|
||||
UUID downloadUuid) {
|
||||
|
||||
return new AuditLogEntity(
|
||||
userId,
|
||||
@@ -82,7 +98,9 @@ public class AuditLogEntity extends CommonCreateEntity {
|
||||
ip,
|
||||
requestUri,
|
||||
null, // requestBody 없음
|
||||
null // errorLogUid 없음
|
||||
null, // errorLogUid 없음
|
||||
downloadUuid,
|
||||
null // loginAttemptId 없음
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,6 @@ public class MapSheetAnalDataInferenceGeomEntity {
|
||||
private Long pnu;
|
||||
|
||||
@Size(max = 20)
|
||||
@ColumnDefault("'0'")
|
||||
@Column(name = "fit_state", length = 20)
|
||||
private String fitState;
|
||||
|
||||
@@ -150,12 +149,6 @@ public class MapSheetAnalDataInferenceGeomEntity {
|
||||
@Column(name = "file_created_dttm")
|
||||
private ZonedDateTime fileCreatedDttm;
|
||||
|
||||
@Column(name = "pass_yn")
|
||||
private String passYn;
|
||||
|
||||
@Column(name = "pass_yn_dttm")
|
||||
private ZonedDateTime passYnDttm;
|
||||
|
||||
@Column(name = "result_uid")
|
||||
private String resultUid;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.kamco.cd.kamcoback.postgres.entity;
|
||||
|
||||
import com.kamco.cd.kamcoback.gukyuin.dto.GukYuinStatus;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
@@ -186,6 +187,12 @@ public class MapSheetLearnEntity {
|
||||
@Column(name = "m3_failed_jobs", nullable = false)
|
||||
private int m3FailedJobs = 0;
|
||||
|
||||
@Column(name = "apply_status")
|
||||
private String applyStatus = GukYuinStatus.PENDING.getId();
|
||||
|
||||
@Column(name = "apply_status_dttm")
|
||||
private ZonedDateTime applyStatusDttm;
|
||||
|
||||
@Column(name = "uid", nullable = false)
|
||||
private String uid = UUID.randomUUID().toString().replace("-", "").toUpperCase();
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.kamco.cd.kamcoback.postgres.repository.Inference;
|
||||
|
||||
import com.kamco.cd.kamcoback.gukyuin.dto.GukYuinDto.GukYuinLinkFacts;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.AnalResultInfo;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.BboxPointDto;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.Dashboard;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.Geom;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.SearchGeoReq;
|
||||
@@ -34,7 +36,11 @@ public interface MapSheetLearnRepositoryCustom {
|
||||
|
||||
AnalResultInfo getInferenceResultInfo(UUID uuid);
|
||||
|
||||
BboxPointDto getBboxPoint(UUID uuid);
|
||||
|
||||
List<Dashboard> getInferenceClassCountList(UUID uuid);
|
||||
|
||||
Page<Geom> getInferenceGeomList(UUID uuid, SearchGeoReq searchGeoReq);
|
||||
|
||||
GukYuinLinkFacts findLinkFacts(UUID uuid);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,10 @@ import static com.kamco.cd.kamcoback.postgres.entity.QSystemMetricEntity.systemM
|
||||
|
||||
import com.kamco.cd.kamcoback.common.exception.CustomApiException;
|
||||
import com.kamco.cd.kamcoback.common.utils.DateRange;
|
||||
import com.kamco.cd.kamcoback.gukyuin.dto.GukYuinDto.GukYuinLinkFacts;
|
||||
import com.kamco.cd.kamcoback.gukyuin.dto.GukYuinStatus;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.AnalResultInfo;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.BboxPointDto;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.Dashboard;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.Geom;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.SearchGeoReq;
|
||||
@@ -20,10 +23,14 @@ import com.kamco.cd.kamcoback.inference.dto.InferenceProgressDto;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.InferenceServerStatusDto;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.InferenceStatusDetailDto;
|
||||
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheetScope;
|
||||
import com.kamco.cd.kamcoback.model.service.ModelMngService;
|
||||
import com.kamco.cd.kamcoback.postgres.entity.MapSheetLearnEntity;
|
||||
import com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalInferenceEntity;
|
||||
import com.kamco.cd.kamcoback.postgres.entity.QMapSheetLearnEntity;
|
||||
import com.kamco.cd.kamcoback.postgres.entity.QModelMngEntity;
|
||||
import com.querydsl.core.BooleanBuilder;
|
||||
import com.querydsl.core.types.Expression;
|
||||
import com.querydsl.core.types.Projections;
|
||||
import com.querydsl.core.types.dsl.BooleanExpression;
|
||||
import com.querydsl.core.types.dsl.CaseBuilder;
|
||||
@@ -319,7 +326,9 @@ public class MapSheetLearnRepositoryImpl implements MapSheetLearnRepositoryCusto
|
||||
mapSheetLearnEntity.inferStartDttm,
|
||||
mapSheetLearnEntity.inferEndDttm,
|
||||
mapSheetLearnEntity.stage,
|
||||
Expressions.stringTemplate("substring({0} from 1 for 8)", mapSheetLearnEntity.uid)))
|
||||
Expressions.stringTemplate("substring({0} from 1 for 8)", mapSheetLearnEntity.uid),
|
||||
mapSheetLearnEntity.applyYn,
|
||||
mapSheetLearnEntity.applyDttm))
|
||||
.from(mapSheetLearnEntity)
|
||||
.leftJoin(m1)
|
||||
.on(mapSheetLearnEntity.m1ModelUuid.eq(m1.uuid))
|
||||
@@ -331,6 +340,30 @@ public class MapSheetLearnRepositoryImpl implements MapSheetLearnRepositoryCusto
|
||||
.fetchOne();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BboxPointDto getBboxPoint(UUID uuid) {
|
||||
Expression<String> bboxGeom =
|
||||
Expressions.stringTemplate(
|
||||
"ST_AsGeoJSON(ST_Envelope(ST_Collect({0})))", mapSheetAnalDataInferenceGeomEntity.geom);
|
||||
|
||||
Expression<String> bboxCenterPoint =
|
||||
Expressions.stringTemplate(
|
||||
"ST_AsGeoJSON(ST_Centroid(ST_Envelope(ST_Collect({0}))))",
|
||||
mapSheetAnalDataInferenceGeomEntity.geom);
|
||||
|
||||
return queryFactory
|
||||
.select(Projections.constructor(BboxPointDto.class, bboxGeom, bboxCenterPoint))
|
||||
.from(mapSheetLearnEntity)
|
||||
.join(mapSheetAnalInferenceEntity)
|
||||
.on(mapSheetAnalInferenceEntity.learnId.eq(mapSheetLearnEntity.id))
|
||||
.join(mapSheetAnalDataInferenceEntity)
|
||||
.on(mapSheetAnalDataInferenceEntity.analUid.eq(mapSheetAnalInferenceEntity.id))
|
||||
.join(mapSheetAnalDataInferenceGeomEntity)
|
||||
.on(mapSheetAnalDataInferenceGeomEntity.dataUid.eq(mapSheetAnalDataInferenceEntity.id))
|
||||
.where(mapSheetLearnEntity.uuid.eq(uuid))
|
||||
.fetchOne();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Dashboard> getInferenceClassCountList(UUID uuid) {
|
||||
|
||||
@@ -420,7 +453,7 @@ public class MapSheetLearnRepositoryImpl implements MapSheetLearnRepositoryCusto
|
||||
|
||||
StringExpression pnu =
|
||||
Expressions.stringTemplate(
|
||||
"coalesce(({0}), '')",
|
||||
"nullif(({0}), '')",
|
||||
JPAExpressions.select(Expressions.stringTemplate("string_agg({0}, ',')", pnuEntity.pnu))
|
||||
.from(pnuEntity)
|
||||
.where(pnuEntity.geo.geoUid.eq(mapSheetAnalDataInferenceGeomEntity.geoUid)));
|
||||
@@ -446,7 +479,7 @@ public class MapSheetLearnRepositoryImpl implements MapSheetLearnRepositoryCusto
|
||||
"substring({0} from 1 for 8)",
|
||||
mapSheetAnalDataInferenceGeomEntity.resultUid),
|
||||
pnu,
|
||||
mapSheetAnalDataInferenceGeomEntity.passYn))
|
||||
mapSheetAnalDataInferenceGeomEntity.fitState))
|
||||
.from(mapSheetAnalInferenceEntity)
|
||||
.join(mapSheetAnalDataInferenceEntity)
|
||||
.on(mapSheetAnalDataInferenceEntity.analUid.eq(mapSheetAnalInferenceEntity.id))
|
||||
@@ -474,4 +507,57 @@ public class MapSheetLearnRepositoryImpl implements MapSheetLearnRepositoryCusto
|
||||
|
||||
return new PageImpl<>(content, pageable, total == null ? 0L : total);
|
||||
}
|
||||
|
||||
/**
|
||||
* 국유in연동 가능여부 확인
|
||||
*
|
||||
* @param uuid uuid
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public GukYuinLinkFacts findLinkFacts(UUID uuid) {
|
||||
|
||||
MapSheetLearnEntity learn =
|
||||
queryFactory
|
||||
.selectFrom(QMapSheetLearnEntity.mapSheetLearnEntity)
|
||||
.where(QMapSheetLearnEntity.mapSheetLearnEntity.uuid.eq(uuid))
|
||||
.fetchOne();
|
||||
|
||||
if (learn == null) {
|
||||
return new GukYuinLinkFacts(false, false, false, false);
|
||||
}
|
||||
|
||||
boolean isPartScope = MapSheetScope.PART.getId().equals(learn.getMapSheetScope());
|
||||
|
||||
QMapSheetAnalInferenceEntity inf = QMapSheetAnalInferenceEntity.mapSheetAnalInferenceEntity;
|
||||
QMapSheetLearnEntity learn2 = new QMapSheetLearnEntity("learn2");
|
||||
QMapSheetLearnEntity learnQ = QMapSheetLearnEntity.mapSheetLearnEntity;
|
||||
|
||||
boolean hasRunningInference =
|
||||
queryFactory
|
||||
.selectOne()
|
||||
.from(inf)
|
||||
.join(learn2)
|
||||
.on(inf.learnId.eq(learn2.id))
|
||||
.where(
|
||||
learn2.compareYyyy.eq(learn.getCompareYyyy()),
|
||||
learn2.targetYyyy.eq(learn.getTargetYyyy()),
|
||||
inf.analState.in("ASSIGNED", "ING"))
|
||||
.fetchFirst()
|
||||
!= null;
|
||||
|
||||
boolean hasOtherUnfinishedGukYuin =
|
||||
queryFactory
|
||||
.selectOne()
|
||||
.from(learnQ)
|
||||
.where(
|
||||
learnQ.compareYyyy.eq(learn.getCompareYyyy()),
|
||||
learnQ.targetYyyy.eq(learn.getTargetYyyy()),
|
||||
learnQ.applyStatus.eq(GukYuinStatus.IN_PROGRESS.getId()),
|
||||
learnQ.uuid.ne(learn.getUuid()))
|
||||
.fetchFirst()
|
||||
!= null;
|
||||
|
||||
return new GukYuinLinkFacts(true, isPartScope, hasRunningInference, hasOtherUnfinishedGukYuin);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,10 +67,8 @@ public class ChangeDetectionRepositoryImpl extends QuerydslRepositorySupport
|
||||
mapSheetAnalSttcEntity.id.classAfterCd.as("classNm"), // 앞단 CoreService 에서 한글명으로 변환
|
||||
mapSheetAnalSttcEntity.classAfterCnt.sum()))
|
||||
.from(mapSheetAnalInferenceEntity)
|
||||
.innerJoin(mapSheetAnalDataInferenceEntity)
|
||||
.on(mapSheetAnalDataInferenceEntity.analUid.eq(mapSheetAnalInferenceEntity.id))
|
||||
.innerJoin(mapSheetAnalSttcEntity)
|
||||
.on(mapSheetAnalSttcEntity.id.dataUid.eq(mapSheetAnalDataInferenceEntity.id))
|
||||
.on(mapSheetAnalSttcEntity.id.analUid.eq(mapSheetAnalInferenceEntity.id))
|
||||
.where(
|
||||
mapSheetAnalInferenceEntity.uuid.eq(uuid),
|
||||
mapScaleTypeSearchExpression(scale, mapSheetNum))
|
||||
|
||||
@@ -1152,7 +1152,7 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
|
||||
// 날짜별 전체 건수
|
||||
Expression<Long> dailyTotalCnt = Expressions.numberTemplate(Long.class, "COUNT(*)");
|
||||
|
||||
// ⭐ 전체 기간 총 건수 (윈도우 함수)
|
||||
// 전체 기간 총 건수 (윈도우 함수)
|
||||
Expression<Long> totalCnt = Expressions.numberTemplate(Long.class, "SUM(COUNT(*)) OVER ()");
|
||||
|
||||
// 상태별 카운트 (Postgres FILTER 사용)
|
||||
@@ -1192,7 +1192,7 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
|
||||
LabelingStatDto.class,
|
||||
workDate,
|
||||
dailyTotalCnt,
|
||||
totalCnt, // ⭐ 전체 일자 배정 건수
|
||||
totalCnt, // 전체 일자 배정 건수
|
||||
assignedCnt,
|
||||
skipCnt,
|
||||
completeCnt,
|
||||
@@ -1233,7 +1233,7 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
|
||||
// 날짜별 전체 건수
|
||||
Expression<Long> dailyTotalCnt = Expressions.numberTemplate(Long.class, "COUNT(*)");
|
||||
|
||||
// ⭐ 전체 기간 총 건수 (윈도우 함수)
|
||||
// 전체 기간 총 건수 (윈도우 함수)
|
||||
Expression<Long> totalCnt = Expressions.numberTemplate(Long.class, "SUM(COUNT(*)) OVER ()");
|
||||
|
||||
// 상태별 카운트 (Postgres FILTER 사용)
|
||||
@@ -1277,7 +1277,7 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
|
||||
LabelingStatDto.class,
|
||||
workDate,
|
||||
dailyTotalCnt,
|
||||
totalCnt, // ⭐ 전체 일자 배정 건수
|
||||
totalCnt, // 전체 일자 배정 건수
|
||||
assignedCnt,
|
||||
skipCnt,
|
||||
completeCnt,
|
||||
|
||||
@@ -488,7 +488,7 @@ public class AuditLogRepositoryImpl extends QuerydslRepositorySupport
|
||||
|
||||
private NumberExpression<Integer> readCount() {
|
||||
return new CaseBuilder()
|
||||
.when(auditLogEntity.eventType.eq(EventType.READ))
|
||||
.when(auditLogEntity.eventType.in(EventType.LIST, EventType.DETAIL))
|
||||
.then(1)
|
||||
.otherwise(0)
|
||||
.sum();
|
||||
@@ -496,7 +496,7 @@ public class AuditLogRepositoryImpl extends QuerydslRepositorySupport
|
||||
|
||||
private NumberExpression<Integer> cudCount() {
|
||||
return new CaseBuilder()
|
||||
.when(auditLogEntity.eventType.in(EventType.CREATE, EventType.UPDATE, EventType.DELETE))
|
||||
.when(auditLogEntity.eventType.in(EventType.ADDED, EventType.MODIFIED, EventType.REMOVE))
|
||||
.then(1)
|
||||
.otherwise(0)
|
||||
.sum();
|
||||
@@ -504,7 +504,7 @@ public class AuditLogRepositoryImpl extends QuerydslRepositorySupport
|
||||
|
||||
private NumberExpression<Integer> printCount() {
|
||||
return new CaseBuilder()
|
||||
.when(auditLogEntity.eventType.eq(EventType.PRINT))
|
||||
.when(auditLogEntity.eventType.eq(EventType.OTHER))
|
||||
.then(1)
|
||||
.otherwise(0)
|
||||
.sum();
|
||||
|
||||
@@ -426,7 +426,7 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
|
||||
"concat({0}, substring({1}, 1, 5))",
|
||||
mapInkx5kEntity.mapidNm, mapSheetMngHstEntity.mapSheetNum),
|
||||
|
||||
// ✅ 튜플 방지: concat으로 문자열 생성
|
||||
// 튜플 방지: concat으로 문자열 생성
|
||||
Expressions.stringTemplate(
|
||||
"concat('(', {0}, ',', {1}, ')')",
|
||||
mapInkx5kEntity.mapidNm, mapSheetMngHstEntity.mapSheetNum),
|
||||
@@ -437,7 +437,7 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
|
||||
// fid 타입 주의 (Long이면 DTO도 Long으로 맞추는 걸 추천)
|
||||
mapInkx5kEntity.fid, // 또는 mapInkx5kEntity.fid.intValue()
|
||||
|
||||
// ✅ createdDate 말고 ZonedDateTime으로 매핑된 필드로
|
||||
// createdDate 말고 ZonedDateTime으로 매핑된 필드로
|
||||
mapSheetMngHstEntity.createdDate, // (예시)
|
||||
mapSheetMngHstEntity.syncState,
|
||||
mapSheetMngHstEntity.syncTfwFileName,
|
||||
|
||||
Reference in New Issue
Block a user