Merge pull request '사용자등록 추가, 추론결과 dashboard 수정' (#15) from feat/demo-20251205 into develop

Reviewed-on: https://kamco.gitea.gs.dabeeo.com/dabeeo/kamco-dabeeo-backoffice/pulls/15
This commit is contained in:
2025-11-26 17:08:16 +09:00
21 changed files with 421 additions and 55 deletions

View File

@@ -57,6 +57,9 @@ dependencies {
// Apache Commons Compress for archive handling
implementation 'org.apache.commons:commons-compress:1.26.0'
// crypto
implementation 'org.springframework.security:spring-security-crypto'
}
tasks.named('test') {

View File

@@ -0,0 +1,55 @@
package com.kamco.cd.kamcoback.auth;
import com.kamco.cd.kamcoback.auth.dto.AuthDto;
import com.kamco.cd.kamcoback.auth.service.AuthService;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "사용자 관리", description = "사용자 관리 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
public class AuthApiController {
private final AuthService authService;
@Operation(summary = "사용자 등록", description = "사용자를 등록 합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "사용자 등록 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/signup")
public ApiResponseDto<Long> signup(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "사용자 정보",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = AuthDto.Signup.class)))
@RequestBody
@Valid
AuthDto.Signup signup) {
return ApiResponseDto.createOK(authService.signup(signup));
}
}

View File

@@ -0,0 +1,81 @@
package com.kamco.cd.kamcoback.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
@RequiredArgsConstructor
public class AuthDto {
@Getter
@Setter
public static class Basic {
private String userAuth;
private String userNm;
private String userId;
private String empId;
private String userEmail;
public Basic(String userAuth, String userNm, String userId, String empId, String userEmail) {
this.userAuth = userAuth;
this.userNm = userNm;
this.userId = userId;
this.empId = empId;
this.userEmail = userEmail;
}
}
@Schema(name = "Signup", description = "사용자 등록 정보")
@Getter
@Setter
public static class Signup {
@Schema(description = "구분", example = "관리자/라벨러/검수자 중 하나")
@NotBlank
private String userAuth;
@NotBlank
@Schema(description = "이름", example = "홍길동")
private String userNm;
@Schema(description = "ID", example = "gildong")
@NotBlank
private String userId;
@Schema(description = "PW", example = "password")
@NotBlank
private String userPw;
@Schema(description = "사번", example = "사번")
@NotBlank
private String empId;
@Schema(description = "이메일", example = "gildong@naver.com")
@NotBlank
private String userEmail;
public Signup(
String userAuth,
String userNm,
String userId,
String userPw,
String empId,
String userEmail) {
this.userAuth = userAuth;
this.userNm = userNm;
this.userId = userId;
this.userPw = userPw;
this.empId = empId;
this.userEmail = userEmail;
}
}
@Getter
public static class User {
String userId;
String userPw;
}
}

View File

@@ -0,0 +1,28 @@
package com.kamco.cd.kamcoback.auth.service;
import com.kamco.cd.kamcoback.auth.dto.AuthDto;
import com.kamco.cd.kamcoback.postgres.core.AuthCoreService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class AuthService {
private final AuthCoreService authCoreService;
private final PasswordEncoder passwordEncoder;
/**
* 사용자 등록
*
* @param signup
* @return
*/
@Transactional
public Long signup(AuthDto.Signup signup) {
signup.setUserPw(passwordEncoder.encode(signup.getUserPw()));
return authCoreService.signup(signup);
}
}

View File

@@ -288,14 +288,11 @@ public class GlobalExceptionHandler {
// TODO : 로그인 개발되면 이것도 연결해야 함
Long userid = Long.valueOf(Optional.ofNullable(ApiLogFunction.getUserId(request)).orElse("1"));
// TODO : stackTrace limit 20줄? 확인 필요
String stackTraceStr =
Arrays.stream(stackTrace)
// .limit(20)
.map(StackTraceElement::toString)
.collect(Collectors.joining("\n"))
.substring(0, Math.min(stackTrace.length, 255));
;
ErrorLogEntity errorLogEntity =
new ErrorLogEntity(

View File

@@ -0,0 +1,15 @@
package com.kamco.cd.kamcoback.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder() {
// strength 기본값 10, 필요하면 조절 가능
return new BCryptPasswordEncoder();
}
}

View File

@@ -2,8 +2,6 @@ package com.kamco.cd.kamcoback.inference;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Dashboard;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Detail;
import com.kamco.cd.kamcoback.inference.service.InferenceResultService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -96,12 +94,7 @@ public class InferenceResultApiController {
@GetMapping("/detail")
public ApiResponseDto<InferenceResultDto.Detail> getInferenceDetail(
@Parameter(description = "목록 id", example = "1") @RequestParam Long id) {
// summary
InferenceResultDto.AnalResSummary summary =
inferenceResultService.getInferenceResultSummary(id);
// dashBoard
List<InferenceResultDto.Dashboard> dashboardList = this.getInferenceResultDashboard(id);
return ApiResponseDto.ok(new Detail(summary, dashboardList));
return ApiResponseDto.ok(inferenceResultService.getDetail(id));
}
@Operation(summary = "추론관리 분석결과 상세 목록", description = "추론관리 분석결과 상세 목록 geojson 데이터 조회")
@@ -139,14 +132,4 @@ public class InferenceResultApiController {
inferenceResultService.getInferenceResultGeomList(searchGeoReq);
return ApiResponseDto.ok(geomList);
}
/**
* 분석결과 상세 대시보드 조회
*
* @param id
* @return
*/
private List<Dashboard> getInferenceResultDashboard(Long id) {
return inferenceResultService.getInferenceResultBasic(id);
}
}

View File

@@ -188,10 +188,12 @@ public class InferenceResultDto {
public static class Detail {
AnalResSummary summary;
List<Dashboard> dashboard;
Long totalCnt;
public Detail(AnalResSummary summary, List<Dashboard> dashboard) {
public Detail(AnalResSummary summary, List<Dashboard> dashboard, Long totalCnt) {
this.summary = summary;
this.dashboard = dashboard;
this.totalCnt = totalCnt;
}
}

View File

@@ -2,6 +2,7 @@ package com.kamco.cd.kamcoback.inference.service;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Dashboard;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Detail;
import com.kamco.cd.kamcoback.postgres.core.InferenceResultCoreService;
import java.util.List;
import lombok.RequiredArgsConstructor;
@@ -57,4 +58,21 @@ public class InferenceResultService {
InferenceResultDto.SearchGeoReq searchGeoReq) {
return inferenceResultCoreService.getInferenceResultGeomList(searchGeoReq);
}
/**
* 분석결과 상제 정보 Summary, DashBoard
*
* @param id
* @return
*/
public Detail getDetail(Long id) {
// summary
InferenceResultDto.AnalResSummary summary = this.getInferenceResultSummary(id);
// 탐지건수 dashBoard
List<InferenceResultDto.Dashboard> dashboardList = this.getInferenceResultBasic(id);
// 전체 탐지건수
Long totalCnt = dashboardList.stream().mapToLong(Dashboard::getClassAfterCnt).sum();
return new Detail(summary, dashboardList, totalCnt);
}
}

View File

@@ -22,7 +22,8 @@ public class QuerydslOrderUtil {
sort -> {
Order order = sort.isAscending() ? Order.ASC : Order.DESC;
// PathBuilder.get()는 컬럼명(String)을 동적 Path로 반환
return new OrderSpecifier<>(order, entityPath.get(sort.getProperty(), String.class));
return new OrderSpecifier<>(
order, entityPath.get(sort.getProperty(), Comparable.class));
})
.toArray(OrderSpecifier[]::new);
}

View File

@@ -0,0 +1,26 @@
package com.kamco.cd.kamcoback.postgres.core;
import com.kamco.cd.kamcoback.auth.dto.AuthDto;
import com.kamco.cd.kamcoback.postgres.repository.auth.AuthRepository;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class AuthCoreService {
private final AuthRepository authRepository;
/**
* 사용자 등록
*
* @param signup
* @return
*/
public Long signup(AuthDto.Signup signup) {
if (authRepository.findByUserId(signup.getUserId()).isPresent()) {
new EntityNotFoundException("중복된 아이디가 있습니다. " + signup.getUserId());
}
return authRepository.signup(signup);
}
}

View File

@@ -4,6 +4,7 @@ import com.kamco.cd.kamcoback.log.dto.ErrorLogDto;
import com.kamco.cd.kamcoback.log.dto.EventType;
import com.kamco.cd.kamcoback.postgres.CommonCreateEntity;
import jakarta.persistence.*;
import jakarta.validation.constraints.Size;
import java.time.ZonedDateTime;
import lombok.AccessLevel;
import lombok.Getter;
@@ -32,7 +33,8 @@ public class ErrorLogEntity extends CommonCreateEntity {
private String errorCode;
private String errorMessage;
@Column(columnDefinition = "TEXT")
@Size(max = 255)
@Column(name = "stack_trace")
private String stackTrace;
private Long handlerUid;

View File

@@ -47,6 +47,9 @@ public class ModelMngEntity extends CommonDateEntity {
@Column(name = "model_cntnt", columnDefinition = "TEXT")
private String modelCntnt;
@Column(name = "bbone_ver")
private String bboneVer;
public ModelMngEntity(
String modelNm,
String modelCate,

View File

@@ -6,9 +6,12 @@ import com.kamco.cd.kamcoback.postgres.CommonDateEntity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.util.Map;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
@Getter
@Setter
@@ -63,8 +66,34 @@ public class ModelVerEntity extends CommonDateEntity {
@Column(name = "updated_uid")
private Long updatedUid;
@Column(name = "deleted")
private Boolean deleted = false;
@Column(name = "leaning_rate", columnDefinition = "float8[]")
private Double[] leaningRate;
@Column(name = "batch_size", columnDefinition = "float8[]")
private Double[] batchSize;
@Column(name = "dropout_rate", columnDefinition = "float8[]")
private Double[] dropoutRate;
@Column(name = "number_of_filters", columnDefinition = "float8[]")
private Double[] numberOfFilters;
@Column(name = "best_state")
private String bestState;
@Column(name = "copy_state")
private String copyState;
@Column(name = "hyper_params")
@JdbcTypeCode(SqlTypes.JSON)
private Map<String, Object> hyperParams;
@Column(name = "ver_cntnt", columnDefinition = "TEXT")
private String verCntnt;
public ModelVerEntity(
Long id,
Long modelUid,

View File

@@ -1,37 +1,86 @@
package com.kamco.cd.kamcoback.postgres.entity;
import com.kamco.cd.kamcoback.postgres.CommonDateEntity;
import jakarta.persistence.*;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.ZonedDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.ColumnDefault;
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "tb_user")
public class UserEntity extends CommonDateEntity {
@Setter
@Entity
@Table(
name = "tb_user",
uniqueConstraints = {@UniqueConstraint(name = "ux_tb_user_user_id", columnNames = "user_id")})
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_uid")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tb_user_id_gen")
@SequenceGenerator(
name = "tb_user_id_gen",
sequenceName = "tb_user_user_uid_seq",
allocationSize = 1)
@Column(name = "user_uid", nullable = false)
private Long id;
@Column(name = "user_nm")
@Size(max = 255)
@NotNull
@Column(name = "user_nm", nullable = false)
private String userNm;
@Column(name = "user_id")
@Size(max = 255)
@NotNull
@Column(name = "user_id", nullable = false)
private String userId;
@Column(name = "pswd")
private String pswd; // TODO: 암호화
@Size(max = 255)
@NotNull
@Column(name = "user_pw", nullable = false)
private String userPw;
// @Enumerated(EnumType.STRING)
private String state; // TODO: 추후 enum -> ACTIVE : 정상, LOCKED : 잠김, EXPIRED : 만료, WITHDRAWAL : 탈퇴
@Size(max = 255)
@NotNull
@ColumnDefault("'ACTIVE'")
@Column(name = "state", nullable = false)
private String state;
@Column(name = "date_withdrawal")
private ZonedDateTime dateWithdrawal;
private String userEmail;
@ColumnDefault("now()")
@Column(name = "created_dttm")
private ZonedDateTime createdDttm;
@ColumnDefault("now()")
@Column(name = "updated_dttm")
private ZonedDateTime updatedDttm;
@Column(name = "created_uid")
private Long createdUid;
@Column(name = "updated_uid")
private Long updatedUid;
@Size(max = 255)
@Column(name = "user_email")
private String userEmail;
@Size(max = 20)
@NotNull
@Column(name = "user_auth", nullable = false, length = 20)
private String userAuth;
@Size(max = 255)
@NotNull
@Column(name = "emp_id", nullable = false)
private String empId;
}

View File

@@ -45,7 +45,6 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC
public Page<InferenceResultDto.AnalResList> getInferenceResultList(
InferenceResultDto.SearchReq searchReq) {
Pageable pageable = searchReq.toPageable();
// "0000" 전체조회
BooleanBuilder builder = new BooleanBuilder();
if (searchReq.getStatCode() != null && !"0000".equals(searchReq.getStatCode())) {

View File

@@ -0,0 +1,6 @@
package com.kamco.cd.kamcoback.postgres.repository.auth;
import com.kamco.cd.kamcoback.postgres.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AuthRepository extends JpaRepository<UserEntity, Long>, AuthRepositoryCustom {}

View File

@@ -0,0 +1,11 @@
package com.kamco.cd.kamcoback.postgres.repository.auth;
import com.kamco.cd.kamcoback.auth.dto.AuthDto;
import com.kamco.cd.kamcoback.postgres.entity.UserEntity;
import java.util.Optional;
public interface AuthRepositoryCustom {
Long signup(AuthDto.Signup signup);
Optional<UserEntity> findByUserId(String userId);
}

View File

@@ -0,0 +1,55 @@
package com.kamco.cd.kamcoback.postgres.repository.auth;
import com.kamco.cd.kamcoback.auth.dto.AuthDto;
import com.kamco.cd.kamcoback.postgres.entity.QUserEntity;
import com.kamco.cd.kamcoback.postgres.entity.UserEntity;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
@Repository
@RequiredArgsConstructor
public class AuthRepositoryImpl implements AuthRepositoryCustom {
private final JPAQueryFactory queryFactory;
private final QUserEntity userEntity = QUserEntity.userEntity;
/**
* 사용자 등록
*
* @param signup
* @return
*/
@Override
public Long signup(AuthDto.Signup signup) {
return queryFactory
.insert(userEntity)
.columns(
userEntity.userAuth,
userEntity.userId,
userEntity.userNm,
userEntity.userPw,
userEntity.userEmail,
userEntity.empId)
.values(
signup.getUserAuth(),
signup.getUserId(),
signup.getUserNm(),
signup.getUserPw(),
signup.getUserEmail(),
signup.getEmpId())
.execute();
}
/**
* 유저 아이디 조회
*
* @param userId
* @return
*/
@Override
public Optional<UserEntity> findByUserId(String userId) {
return Optional.ofNullable(
queryFactory.selectFrom(userEntity).where(userEntity.userId.eq(userId)).fetchOne());
}
}

View File

@@ -12,18 +12,15 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
public class CommonCodeRepositoryImpl extends QuerydslRepositorySupport
implements CommonCodeRepositoryCustom {
@Repository
@RequiredArgsConstructor
public class CommonCodeRepositoryImpl implements CommonCodeRepositoryCustom {
private final JPAQueryFactory queryFactory;
public CommonCodeRepositoryImpl(JPAQueryFactory queryFactory) {
super(CommonCodeEntity.class);
this.queryFactory = queryFactory;
}
@Override
public Optional<CommonCodeEntity> findByCodeId(Long id) {
QCommonCodeEntity child = new QCommonCodeEntity("child");

View File

@@ -52,10 +52,13 @@ public class ModelMngRepositoryImpl extends QuerydslRepositorySupport
modelMngEntity.modelCate,
modelVerEntity.id.as("modelVerUid"),
modelVerEntity.modelVer,
modelVerEntity.usedState,
modelVerEntity.modelState,
Expressions.stringTemplate(
"fn_codenm_to_misc({0}, {1})", 51, modelVerEntity.usedState), // 사용여부 한글 명칭
Expressions.stringTemplate(
"fn_codenm_to_misc({0}, {1})", 51, modelVerEntity.modelState), // 모델상태 한글 명칭
modelVerEntity.qualityProb,
modelVerEntity.deployState,
Expressions.stringTemplate(
"fn_codenm_to_misc({0}, {1})", 52, modelVerEntity.deployState), // 배포상태 한글 명칭
modelVerEntity.modelPath))
.from(modelMngEntity)
.innerJoin(modelVerEntity)
@@ -82,8 +85,11 @@ public class ModelMngRepositoryImpl extends QuerydslRepositorySupport
Expressions.stringTemplate(
"to_char({0}, 'YYYY-MM-DD')", modelVerEntity.createdDate)
.as("createdDttm"),
modelVerEntity.usedState,
modelVerEntity.deployState,
Expressions.stringTemplate(
"fn_codenm_to_misc({0}, {1})", 51, modelVerEntity.usedState), // 사용여부 한글 명칭
Expressions.stringTemplate(
"fn_codenm_to_misc({0}, {1})",
52, modelVerEntity.deployState), // 배포상태 한글 명칭
Expressions.stringTemplate(
"to_char({0}, 'YYYY-MM-DD')", modelDeployHstEntity.deployDttm)
.as("deployDttm")))