feat: UIMS 로그인보조+권한관리(RBAC) 이식
This commit is contained in:
parent
f264524a87
commit
13d1f53eed
@ -0,0 +1,43 @@
|
|||||||
|
package com.zioinfo.esn.auth;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인 보조 3종 컨트롤러(공개 — SecurityConfig: /api/auth/** permitAll).
|
||||||
|
* - POST /api/auth/signup : 회원가입(승인대기)
|
||||||
|
* - POST /api/auth/find-id : 아이디 찾기(이름+이메일)
|
||||||
|
* - POST /api/auth/reset-password : 비밀번호 초기화(임시비번 메일 발송)
|
||||||
|
* AuthController(/login,/verify,/me,/logout)와 메서드 경로 미충돌 — 동일 prefix 분리 컨트롤러.
|
||||||
|
*
|
||||||
|
* 보안: 아이디찾기/비번초기화는 미존재 계정도 success=true(열거방지).
|
||||||
|
* 임시비번/자격증명은 응답에 절대 미포함(메일/로그 채널 전용).
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/auth")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AuthHelperController {
|
||||||
|
|
||||||
|
private final AuthHelperService authHelperService;
|
||||||
|
|
||||||
|
@PostMapping("/signup")
|
||||||
|
public ApiResponse<SignupResponse> signup(@RequestBody SignupRequest req) {
|
||||||
|
return ApiResponse.ok("가입 신청 완료 (승인 대기)", authHelperService.signup(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/find-id")
|
||||||
|
public ApiResponse<FindIdResponse> findId(@RequestBody FindIdRequest req) {
|
||||||
|
return ApiResponse.ok(authHelperService.findId(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/reset-password")
|
||||||
|
public ApiResponse<Void> resetPassword(@RequestBody ResetPwRequest req) {
|
||||||
|
// 미존재해도 항상 success=true (열거방지). 임시비번은 메일/로그로만 전달.
|
||||||
|
authHelperService.resetPassword(req);
|
||||||
|
return ApiResponse.ok("임시 비밀번호를 이메일로 발송했습니다.", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,117 @@
|
|||||||
|
package com.zioinfo.esn.auth;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.auth.mapper.UserAuthMapper;
|
||||||
|
import com.zioinfo.esn.uiws.common.mail.MailSender;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인 보조 3종(회원가입 승인대기 / 아이디찾기 / 비밀번호 초기화).
|
||||||
|
* 기존 AuthService(2FA 로그인)는 손대지 않고 별도 서비스로 분리.
|
||||||
|
*
|
||||||
|
* 보안(불변규칙):
|
||||||
|
* - 임시비밀번호/비밀번호/자격증명은 API 응답·예외 메시지에 절대 미노출. 메일/로그 채널로만 전달.
|
||||||
|
* - 아이디찾기/비번초기화는 미존재 계정도 success=true(found=false) — 계정 열거(enumeration) 방지.
|
||||||
|
* - 회원가입은 승인대기(is_active=FALSE, approval_status='PENDING') — 즉시 활성화 없음.
|
||||||
|
* - 외부 API 호출 없음(MailSender 폴백은 로그). Ollama 외 외부 통신 금지 준수.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AuthHelperService {
|
||||||
|
|
||||||
|
private static final SecureRandom RANDOM = new SecureRandom();
|
||||||
|
private static final String TEMP_PW_ALPHABET =
|
||||||
|
"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
|
||||||
|
private static final int TEMP_PW_LEN = 12;
|
||||||
|
|
||||||
|
private final UserAuthMapper userMapper;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final MailSender mailSender;
|
||||||
|
|
||||||
|
/** 회원가입 — 승인대기 등록. 아이디 중복 시 거부. */
|
||||||
|
@Transactional
|
||||||
|
public SignupResponse signup(SignupRequest req) {
|
||||||
|
if (req == null || req.username() == null || req.username().isBlank()
|
||||||
|
|| req.password() == null || req.password().isBlank()) {
|
||||||
|
throw new RuntimeException("ERR-AUTH-100: 아이디/비밀번호는 필수입니다.");
|
||||||
|
}
|
||||||
|
if (userMapper.countByUsername(req.username()) > 0) {
|
||||||
|
throw new RuntimeException("ERR-AUTH-101: 이미 사용 중인 아이디");
|
||||||
|
}
|
||||||
|
String hash = passwordEncoder.encode(req.password());
|
||||||
|
userMapper.insertPendingUser(
|
||||||
|
req.tenantCode(), req.username(), hash,
|
||||||
|
req.name(), req.email(), req.phone());
|
||||||
|
log.info("[signup] pending user created username={} (approval required)", req.username());
|
||||||
|
return new SignupResponse(req.username(), "PENDING_APPROVAL");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 아이디 찾기 — 이름+이메일 매칭. 미존재도 200(found=false), 마스킹 반환. */
|
||||||
|
public FindIdResponse findId(FindIdRequest req) {
|
||||||
|
if (req == null || req.name() == null || req.email() == null) {
|
||||||
|
return new FindIdResponse(false, "");
|
||||||
|
}
|
||||||
|
String username = userMapper.findUsernameByNameEmail(req.name(), req.email());
|
||||||
|
if (username == null || username.isBlank()) {
|
||||||
|
return new FindIdResponse(false, "");
|
||||||
|
}
|
||||||
|
return new FindIdResponse(true, maskUsername(username));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 비밀번호 초기화 — 아이디+이름+이메일 일치 시 임시비번 발급(BCrypt 갱신) + 메일 발송.
|
||||||
|
* 미존재해도 예외 없이 반환(열거방지). 임시비번은 메일/로그로만 전달.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void resetPassword(ResetPwRequest req) {
|
||||||
|
if (req == null || req.username() == null || req.name() == null || req.email() == null) {
|
||||||
|
return; // 열거방지 — 잘못된 요청도 조용히 성공 처리
|
||||||
|
}
|
||||||
|
EsnUser user = userMapper.findByUsernameNameEmail(req.username(), req.name(), req.email());
|
||||||
|
if (user == null) {
|
||||||
|
log.info("[reset-pw] no match for username={} (silently success — enumeration guard)",
|
||||||
|
req.username());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String tempPw = generateTempPassword();
|
||||||
|
userMapper.updatePasswordHash(user.getUsername(), passwordEncoder.encode(tempPw));
|
||||||
|
|
||||||
|
String subject = "[GUARDiA ESN] 임시 비밀번호 발급";
|
||||||
|
String body = String.format(
|
||||||
|
"안녕하세요 %s 님,\n임시 비밀번호는 [%s] 입니다.\n로그인 후 즉시 변경해 주세요.",
|
||||||
|
user.getUsername(), tempPw);
|
||||||
|
if (user.getEmail() != null && !user.getEmail().isBlank()) {
|
||||||
|
// 임시비번은 메일 본문에만 — API 응답/예외 메시지에는 절대 미포함.
|
||||||
|
mailSender.send(user.getEmail(), subject, body);
|
||||||
|
} else {
|
||||||
|
log.warn("[reset-pw] no email for username={} — temp password issued (log only)",
|
||||||
|
user.getUsername());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 임시 비밀번호 생성(혼동 문자 제외 영숫자). */
|
||||||
|
private static String generateTempPassword() {
|
||||||
|
StringBuilder sb = new StringBuilder(TEMP_PW_LEN);
|
||||||
|
for (int i = 0; i < TEMP_PW_LEN; i++) {
|
||||||
|
sb.append(TEMP_PW_ALPHABET.charAt(RANDOM.nextInt(TEMP_PW_ALPHABET.length())));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 아이디 마스킹: 앞 2자만 노출 + 나머지 '*'. (2자 이하는 첫 글자만 노출.) */
|
||||||
|
private static String maskUsername(String username) {
|
||||||
|
if (username == null || username.isBlank()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (username.length() <= 2) {
|
||||||
|
return username.charAt(0) + "*";
|
||||||
|
}
|
||||||
|
return username.substring(0, 2) + "*".repeat(username.length() - 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,6 +16,11 @@ public class EsnUser {
|
|||||||
private String email;
|
private String email;
|
||||||
private String phone;
|
private String phone;
|
||||||
private boolean active;
|
private boolean active;
|
||||||
|
// ── 로그인 보조 이식 컬럼 (esn_user ALTER, db/93_esn_login_helper.sql) ─────────
|
||||||
|
/** 이름(아이디찾기/비번초기화 매칭 키). */
|
||||||
|
private String name;
|
||||||
|
/** 가입 승인 상태(APPROVED / PENDING). 신규 가입은 PENDING(승인대기). */
|
||||||
|
private String approvalStatus;
|
||||||
private LocalDateTime lastLoginAt;
|
private LocalDateTime lastLoginAt;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.zioinfo.esn.auth;
|
||||||
|
|
||||||
|
/** 아이디 찾기 요청(이름+이메일 매칭). */
|
||||||
|
public record FindIdRequest(String name, String email) {
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.zioinfo.esn.auth;
|
||||||
|
|
||||||
|
/** 아이디 찾기 응답. found=false 면 maskedUsername="" (열거방지 — 미존재도 200). */
|
||||||
|
public record FindIdResponse(boolean found, String maskedUsername) {
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.zioinfo.esn.auth;
|
||||||
|
|
||||||
|
/** 비밀번호 초기화 요청(아이디+이름+이메일 매칭). 임시비번은 메일/로그로만 전달. */
|
||||||
|
public record ResetPwRequest(String username, String name, String email) {
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.zioinfo.esn.auth;
|
||||||
|
|
||||||
|
/** 회원가입 요청(승인대기 INSERT). 비밀번호는 BCrypt 저장, 응답/로그에 평문 노출 금지. */
|
||||||
|
public record SignupRequest(
|
||||||
|
String username,
|
||||||
|
String password,
|
||||||
|
String name,
|
||||||
|
String email,
|
||||||
|
String phone,
|
||||||
|
String tenantCode) {
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.zioinfo.esn.auth;
|
||||||
|
|
||||||
|
/** 회원가입 응답. status="PENDING_APPROVAL" (승인대기). 자격증명 미포함. */
|
||||||
|
public record SignupResponse(String username, String status) {
|
||||||
|
}
|
||||||
@ -29,4 +29,29 @@ public interface UserAuthMapper {
|
|||||||
|
|
||||||
/** 관리자 잠금 해제(실패 카운트/잠금 초기화). */
|
/** 관리자 잠금 해제(실패 카운트/잠금 초기화). */
|
||||||
int unlock(@Param("username") String username);
|
int unlock(@Param("username") String username);
|
||||||
|
|
||||||
|
// ── 로그인 보조 이식: 회원가입(승인대기) / 아이디찾기 / 비밀번호 초기화 ──────────
|
||||||
|
|
||||||
|
/** 아이디 중복 확인. */
|
||||||
|
int countByUsername(@Param("username") String username);
|
||||||
|
|
||||||
|
/** 회원가입 — 승인대기(is_active=FALSE, approval_status='PENDING') INSERT. */
|
||||||
|
int insertPendingUser(@Param("tenantCode") String tenantCode,
|
||||||
|
@Param("username") String username,
|
||||||
|
@Param("passwordHash") String passwordHash,
|
||||||
|
@Param("name") String name,
|
||||||
|
@Param("email") String email,
|
||||||
|
@Param("phone") String phone);
|
||||||
|
|
||||||
|
/** 아이디 찾기 — 이름+이메일 일치 username 1건(없으면 null). */
|
||||||
|
String findUsernameByNameEmail(@Param("name") String name, @Param("email") String email);
|
||||||
|
|
||||||
|
/** 비밀번호 초기화 — 아이디+이름+이메일 모두 일치하는 사용자(없으면 null). */
|
||||||
|
EsnUser findByUsernameNameEmail(@Param("username") String username,
|
||||||
|
@Param("name") String name,
|
||||||
|
@Param("email") String email);
|
||||||
|
|
||||||
|
/** 임시 비밀번호(BCrypt) 갱신. */
|
||||||
|
int updatePasswordHash(@Param("username") String username,
|
||||||
|
@Param("passwordHash") String passwordHash);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,8 @@ public class SecurityConfig {
|
|||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
// 인증 불필요
|
// 인증 불필요
|
||||||
.requestMatchers("/api/auth/**").permitAll()
|
.requestMatchers("/api/auth/**").permitAll()
|
||||||
|
// UIWS system 이식: 공통 룩업(부서/거래처 트리 등 비민감) 공개 조회
|
||||||
|
.requestMatchers("/api/public/**").permitAll()
|
||||||
.requestMatchers("/actuator/health").permitAll()
|
.requestMatchers("/actuator/health").permitAll()
|
||||||
// 정적 리소스 (React SPA)
|
// 정적 리소스 (React SPA)
|
||||||
.requestMatchers(HttpMethod.GET, "/", "/index.html", "/assets/**", "/*.js", "/*.css", "/*.ico").permitAll()
|
.requestMatchers(HttpMethod.GET, "/", "/index.html", "/assets/**", "/*.js", "/*.css", "/*.ico").permitAll()
|
||||||
@ -44,6 +46,9 @@ public class SecurityConfig {
|
|||||||
.requestMatchers("/api/tenants/**").hasRole("ADMIN")
|
.requestMatchers("/api/tenants/**").hasRole("ADMIN")
|
||||||
.requestMatchers("/api/users/**").hasAnyRole("ADMIN", "MANAGER")
|
.requestMatchers("/api/users/**").hasAnyRole("ADMIN", "MANAGER")
|
||||||
.requestMatchers("/api/firmware/**").hasAnyRole("ADMIN", "MANAGER")
|
.requestMatchers("/api/firmware/**").hasAnyRole("ADMIN", "MANAGER")
|
||||||
|
// UIWS system(권한관리: 사용자/역할/메뉴/부서/거래처/코드/프로그램) — 관리자/매니저 전용
|
||||||
|
// (일반 /api/** 메서드 규칙보다 먼저 평가되도록 상단 배치)
|
||||||
|
.requestMatchers("/api/system/**").hasAnyRole("ADMIN", "MANAGER")
|
||||||
// admin 보조: 감사로그 조회·설정 조회 = ADMIN/MANAGER, 설정 변경 = ADMIN
|
// admin 보조: 감사로그 조회·설정 조회 = ADMIN/MANAGER, 설정 변경 = ADMIN
|
||||||
// (아래 일반 PUT /api/** 규칙보다 먼저 평가되도록 상단 배치)
|
// (아래 일반 PUT /api/** 규칙보다 먼저 평가되도록 상단 배치)
|
||||||
.requestMatchers(HttpMethod.PUT, "/api/admin/settings/**").hasRole("ADMIN")
|
.requestMatchers(HttpMethod.PUT, "/api/admin/settings/**").hasRole("ADMIN")
|
||||||
|
|||||||
@ -36,7 +36,19 @@ public enum UiwsErrorCode {
|
|||||||
// auth (2FA)
|
// auth (2FA)
|
||||||
VERIFY_TOKEN_INVALID("ERR-UIWS-2FA-401", "2차 검증 토큰이 유효하지 않거나 만료되었습니다."),
|
VERIFY_TOKEN_INVALID("ERR-UIWS-2FA-401", "2차 검증 토큰이 유효하지 않거나 만료되었습니다."),
|
||||||
VERIFY_CODE_INVALID("ERR-UIWS-2FA-401C", "인증 코드가 올바르지 않거나 만료되었습니다."),
|
VERIFY_CODE_INVALID("ERR-UIWS-2FA-401C", "인증 코드가 올바르지 않거나 만료되었습니다."),
|
||||||
ACCOUNT_LOCKED("ERR-UIWS-2FA-LOCK", "로그인 실패 횟수 초과로 계정이 잠겼습니다. 관리자에게 문의하세요.");
|
ACCOUNT_LOCKED("ERR-UIWS-2FA-LOCK", "로그인 실패 횟수 초과로 계정이 잠겼습니다. 관리자에게 문의하세요."),
|
||||||
|
|
||||||
|
// system(권한관리) — UIWS system 모듈 이식
|
||||||
|
DUPLICATE_KEY("ERR-UIWS-SYS-409D", "이미 존재하는 항목입니다."),
|
||||||
|
RESOURCE_IN_USE("ERR-UIWS-SYS-409U", "참조 중인 항목이 있어 처리할 수 없습니다."),
|
||||||
|
ROLE_NOT_FOUND("ERR-UIWS-SYS-RL404", "권한을 찾을 수 없습니다."),
|
||||||
|
USER_NOT_FOUND("ERR-UIWS-SYS-US404", "사용자를 찾을 수 없습니다."),
|
||||||
|
USER_ID_DUPLICATED("ERR-UIWS-SYS-US409", "이미 사용 중인 사용자 ID입니다."),
|
||||||
|
DEPT_NOT_FOUND("ERR-UIWS-SYS-DP404", "부서를 찾을 수 없습니다."),
|
||||||
|
COMPANY_NOT_FOUND("ERR-UIWS-SYS-CO404", "거래처를 찾을 수 없습니다."),
|
||||||
|
MENU_NOT_FOUND("ERR-UIWS-SYS-MN404", "메뉴를 찾을 수 없습니다."),
|
||||||
|
PROGRAM_NOT_FOUND("ERR-UIWS-SYS-PG404", "프로그램을 찾을 수 없습니다."),
|
||||||
|
CODE_GRP_NOT_FOUND("ERR-UIWS-SYS-CG404", "코드그룹을 찾을 수 없습니다.");
|
||||||
|
|
||||||
private final String code;
|
private final String code;
|
||||||
private final String message;
|
private final String message;
|
||||||
|
|||||||
@ -0,0 +1,58 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CodeGrpDetailDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CodeGrpDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CodeGrpSaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CodeValueDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.CodeService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 2.7 코드(codes) — 그룹목록/상세/CRUD/드롭다운값. base: /api/system/codes */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/system/codes")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CodeController {
|
||||||
|
|
||||||
|
private final CodeService codeService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResponse<CodeGrpDto>> listGroups(
|
||||||
|
@RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(codeService.listGroups(keyword, page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 드롭다운 공용 코드값 조회(정적 경로 우선). */
|
||||||
|
@GetMapping("/group/{grpCd}/values")
|
||||||
|
public ApiResponse<List<CodeValueDto>> getValues(@PathVariable("grpCd") String grpCd) {
|
||||||
|
return ApiResponse.ok(codeService.getValues(grpCd));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{grpCd}")
|
||||||
|
public ApiResponse<CodeGrpDetailDto> getGroup(@PathVariable("grpCd") String grpCd) {
|
||||||
|
return ApiResponse.ok(codeService.getGroup(grpCd));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<CodeGrpDetailDto> create(@Valid @RequestBody CodeGrpSaveDto dto) {
|
||||||
|
return ApiResponse.ok(codeService.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{grpCd}")
|
||||||
|
public ApiResponse<CodeGrpDetailDto> update(@PathVariable("grpCd") String grpCd, @Valid @RequestBody CodeGrpSaveDto dto) {
|
||||||
|
return ApiResponse.ok(codeService.update(grpCd, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{grpCd}")
|
||||||
|
public ApiResponse<Void> delete(@PathVariable("grpCd") String grpCd) {
|
||||||
|
codeService.delete(grpCd);
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CompanyDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CompanySaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.IdsRequest;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.CompanyService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 2.4 거래처(companies) — 목록/검색팝업/CRUD/다중삭제. base: /api/system/companies */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/system/companies")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CompanyController {
|
||||||
|
|
||||||
|
private final CompanyService companyService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResponse<CompanyDto>> list(
|
||||||
|
@RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(companyService.list(keyword, page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/search")
|
||||||
|
public ApiResponse<List<CompanyDto>> search(@RequestParam(required = false) String keyword) {
|
||||||
|
return ApiResponse.ok(companyService.searchPopup(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<CompanyDto> create(@Valid @RequestBody CompanySaveDto dto) {
|
||||||
|
return ApiResponse.ok(companyService.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<CompanyDto> get(@PathVariable("id") String id) {
|
||||||
|
return ApiResponse.ok(companyService.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ApiResponse<CompanyDto> update(@PathVariable("id") String id, @Valid @RequestBody CompanySaveDto dto) {
|
||||||
|
return ApiResponse.ok(companyService.update(id, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping
|
||||||
|
public ApiResponse<Void> delete(@Valid @RequestBody IdsRequest req) {
|
||||||
|
companyService.delete(req.ids());
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.DeptDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.DeptSaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.DeptTreeDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.DeptService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 2.3 부서(depts) — 목록/검색팝업/트리/CRUD. base: /api/system/depts */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/system/depts")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DeptController {
|
||||||
|
|
||||||
|
private final DeptService deptService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResponse<DeptDto>> list(
|
||||||
|
@RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(deptService.list(keyword, page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/search")
|
||||||
|
public ApiResponse<List<DeptDto>> search(@RequestParam(required = false) String keyword) {
|
||||||
|
return ApiResponse.ok(deptService.searchPopup(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/tree")
|
||||||
|
public ApiResponse<List<DeptTreeDto>> tree() {
|
||||||
|
return ApiResponse.ok(deptService.tree());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<DeptDto> create(@Valid @RequestBody DeptSaveDto dto) {
|
||||||
|
return ApiResponse.ok(deptService.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<DeptDto> get(@PathVariable("id") String id) {
|
||||||
|
return ApiResponse.ok(deptService.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ApiResponse<DeptDto> update(@PathVariable("id") String id, @Valid @RequestBody DeptSaveDto dto) {
|
||||||
|
return ApiResponse.ok(deptService.update(id, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ApiResponse<Void> delete(@PathVariable("id") String id) {
|
||||||
|
deptService.delete(id);
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.DeptUserRoleDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.RoleIdsRequest;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.DeptRoleService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/** 2.2 부서권한(dept-role) — 부서 사용자+권한 조회 / 부여 / 삭제. base: /api/system/depts/{deptId}/roles */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/system/depts/{deptId}/roles")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DeptRoleController {
|
||||||
|
|
||||||
|
private final DeptRoleService deptRoleService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResponse<DeptUserRoleDto>> list(
|
||||||
|
@PathVariable("deptId") String deptId,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(deptRoleService.listDeptUsers(deptId, page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<Void> grant(@PathVariable("deptId") String deptId, @Valid @RequestBody RoleIdsRequest req) {
|
||||||
|
deptRoleService.grant(deptId, req.roleIds());
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping
|
||||||
|
public ApiResponse<Void> revoke(@PathVariable("deptId") String deptId, @Valid @RequestBody RoleIdsRequest req) {
|
||||||
|
deptRoleService.revoke(deptId, req.roleIds());
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.IdsRequest;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.MenuDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.MenuSaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.MenuService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 2.9 메뉴(menus) — 트리목록/검색/CRUD/다중삭제. base: /api/system/menus */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/system/menus")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MenuController {
|
||||||
|
|
||||||
|
private final MenuService menuService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResponse<MenuDto>> list(
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "100") int size) {
|
||||||
|
return ApiResponse.ok(menuService.list(page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/search")
|
||||||
|
public ApiResponse<List<MenuDto>> search(@RequestParam(required = false) String keyword) {
|
||||||
|
return ApiResponse.ok(menuService.searchPopup(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<MenuDto> create(@Valid @RequestBody MenuSaveDto dto) {
|
||||||
|
return ApiResponse.ok(menuService.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<MenuDto> get(@PathVariable("id") String id) {
|
||||||
|
return ApiResponse.ok(menuService.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ApiResponse<MenuDto> update(@PathVariable("id") String id, @Valid @RequestBody MenuSaveDto dto) {
|
||||||
|
return ApiResponse.ok(menuService.update(id, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping
|
||||||
|
public ApiResponse<Void> delete(@Valid @RequestBody IdsRequest req) {
|
||||||
|
menuService.delete(req.ids());
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.IdsRequest;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.ProgramDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.ProgramSaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.ProgramService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 2.8 프로그램(programs) — 목록/검색/CRUD/다중삭제. base: /api/system/programs */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/system/programs")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ProgramController {
|
||||||
|
|
||||||
|
private final ProgramService programService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResponse<ProgramDto>> list(
|
||||||
|
@RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(required = false) String programType,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(programService.list(keyword, programType, page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/search")
|
||||||
|
public ApiResponse<List<ProgramDto>> search(@RequestParam(required = false) String keyword) {
|
||||||
|
return ApiResponse.ok(programService.searchPopup(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<ProgramDto> create(@Valid @RequestBody ProgramSaveDto dto) {
|
||||||
|
return ApiResponse.ok(programService.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<ProgramDto> get(@PathVariable("id") String id) {
|
||||||
|
return ApiResponse.ok(programService.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ApiResponse<ProgramDto> update(@PathVariable("id") String id, @Valid @RequestBody ProgramSaveDto dto) {
|
||||||
|
return ApiResponse.ok(programService.update(id, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping
|
||||||
|
public ApiResponse<Void> delete(@Valid @RequestBody IdsRequest req) {
|
||||||
|
programService.delete(req.ids());
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PublicCompanyDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PublicDeptDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.PublicLookupService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 가입 화면(비인증) 공개 조회. base: /api/public/{depts,companies} */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/public")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PublicLookupController {
|
||||||
|
|
||||||
|
private final PublicLookupService publicLookupService;
|
||||||
|
|
||||||
|
@GetMapping("/depts")
|
||||||
|
public ApiResponse<List<PublicDeptDto>> depts() {
|
||||||
|
return ApiResponse.ok(publicLookupService.depts());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/companies")
|
||||||
|
public ApiResponse<List<PublicCompanyDto>> companies() {
|
||||||
|
return ApiResponse.ok(publicLookupService.companies());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.IdsRequest;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.RoleDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.RoleSaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.RoleService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/** 2.1 권한(roles) — 목록/등록/상세/수정/다중삭제. base: /api/system/roles */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/system/roles")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RoleController {
|
||||||
|
|
||||||
|
private final RoleService roleService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResponse<RoleDto>> list(
|
||||||
|
@RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(roleService.list(keyword, page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<RoleDto> create(@Valid @RequestBody RoleSaveDto dto) {
|
||||||
|
return ApiResponse.ok(roleService.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<RoleDto> get(@PathVariable("id") String id) {
|
||||||
|
return ApiResponse.ok(roleService.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ApiResponse<RoleDto> update(@PathVariable("id") String id, @Valid @RequestBody RoleSaveDto dto) {
|
||||||
|
return ApiResponse.ok(roleService.update(id, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping
|
||||||
|
public ApiResponse<Void> delete(@Valid @RequestBody IdsRequest req) {
|
||||||
|
roleService.delete(req.ids());
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.RoleDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.RoleMenuDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.RoleMenuSaveRequest;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.RoleMenuService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 2.5 메뉴생성(role-menus) — 권한목록 / 권한별 메뉴매핑 조회·저장. */
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RoleMenuController {
|
||||||
|
|
||||||
|
private final RoleMenuService roleMenuService;
|
||||||
|
|
||||||
|
@GetMapping("/api/system/role-menus")
|
||||||
|
public ApiResponse<PageResponse<RoleDto>> listRoles(
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(roleMenuService.listRoles(page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/api/system/roles/{roleId}/menus")
|
||||||
|
public ApiResponse<List<RoleMenuDto>> getRoleMenus(@PathVariable("roleId") String roleId) {
|
||||||
|
return ApiResponse.ok(roleMenuService.getRoleMenus(roleId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/api/system/roles/{roleId}/menus")
|
||||||
|
public ApiResponse<Void> saveRoleMenus(@PathVariable("roleId") String roleId, @Valid @RequestBody RoleMenuSaveRequest req) {
|
||||||
|
roleMenuService.saveRoleMenus(roleId, req.menus());
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CheckIdResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.IdsRequest;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.UserDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.UserSaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.UserService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 2.6 사용자(users) — 목록/검색/중복ID확인/CRUD/다중삭제/비번초기화/잠금해제/승인. base: /api/system/users */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/system/users")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResponse<UserDto>> list(
|
||||||
|
@RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(required = false) String deptId,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(userService.list(keyword, deptId, page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/search")
|
||||||
|
public ApiResponse<List<UserDto>> search(
|
||||||
|
@RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(required = false) String deptId) {
|
||||||
|
return ApiResponse.ok(userService.searchPopup(keyword, deptId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/check-id")
|
||||||
|
public ApiResponse<CheckIdResponse> checkId(@RequestParam String userId) {
|
||||||
|
return ApiResponse.ok(userService.checkId(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<UserDto> create(@Valid @RequestBody UserSaveDto dto) {
|
||||||
|
return ApiResponse.ok(userService.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<UserDto> get(@PathVariable("id") String id) {
|
||||||
|
return ApiResponse.ok(userService.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ApiResponse<UserDto> update(@PathVariable("id") String id, @Valid @RequestBody UserSaveDto dto) {
|
||||||
|
return ApiResponse.ok(userService.update(id, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping
|
||||||
|
public ApiResponse<Void> delete(@Valid @RequestBody IdsRequest req) {
|
||||||
|
userService.delete(req.ids());
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/reset-pw")
|
||||||
|
public ApiResponse<Void> resetPassword(@PathVariable("id") String id) {
|
||||||
|
userService.resetPassword(id);
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/unlock")
|
||||||
|
public ApiResponse<Void> unlock(@PathVariable("id") String id) {
|
||||||
|
userService.unlock(id);
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/approve")
|
||||||
|
public ApiResponse<Void> approve(@PathVariable("id") String id) {
|
||||||
|
userService.approve(id);
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/revoke-approval")
|
||||||
|
public ApiResponse<Void> revokeApproval(@PathVariable("id") String id) {
|
||||||
|
userService.revokeApproval(id);
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** { available: boolean } */
|
||||||
|
public record CheckIdResponse(boolean available) {}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** { grpCd, grpNm, useYn, values: CodeValueDto[] } */
|
||||||
|
public record CodeGrpDetailDto(String grpCd, String grpNm, String useYn, List<CodeValueDto> values) {}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** { grpCd, grpNm, useYn } */
|
||||||
|
public record CodeGrpDto(String grpCd, String grpNm, String useYn) {}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** { grpCd, grpNm, useYn, values: CodeValueDto[] } */
|
||||||
|
public record CodeGrpSaveDto(
|
||||||
|
@NotBlank(message = "grpCd는 필수입니다.") String grpCd,
|
||||||
|
@NotBlank(message = "grpNm은 필수입니다.") String grpNm,
|
||||||
|
@NotBlank(message = "useYn은 필수입니다.") String useYn,
|
||||||
|
List<CodeValueDto> values) {}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** { grpCd, codeVal, codeNm, sortOrd, useYn } */
|
||||||
|
public record CodeValueDto(String grpCd, String codeVal, String codeNm, Integer sortOrd, String useYn) {}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** { companyId, companyNm, bizNo, useYn } */
|
||||||
|
public record CompanyDto(String companyId, String companyNm, String bizNo, String useYn) {}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/** { companyId, companyNm, bizNo, useYn } */
|
||||||
|
public record CompanySaveDto(
|
||||||
|
String companyId,
|
||||||
|
@NotBlank(message = "companyNm은 필수입니다.") String companyNm,
|
||||||
|
String bizNo,
|
||||||
|
@NotBlank(message = "useYn은 필수입니다.") String useYn) {}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** { deptId, deptNm, parentDeptId, sortOrd, useYn } */
|
||||||
|
public record DeptDto(String deptId, String deptNm, String parentDeptId, Integer sortOrd, String useYn) {}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/** { deptId, deptNm, parentDeptId, sortOrd, useYn } */
|
||||||
|
public record DeptSaveDto(
|
||||||
|
String deptId,
|
||||||
|
@NotBlank(message = "deptNm은 필수입니다.") String deptNm,
|
||||||
|
String parentDeptId,
|
||||||
|
Integer sortOrd,
|
||||||
|
@NotBlank(message = "useYn은 필수입니다.") String useYn) {}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 부서 계층 트리 노드. children 은 sortOrd→deptId 순. */
|
||||||
|
public record DeptTreeDto(
|
||||||
|
String deptId, String deptNm, String parentDeptId,
|
||||||
|
Integer sortOrd, String useYn, List<DeptTreeDto> children) {}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** { userId, userNm, roleIds } — 부서 사용자별 부여 권한. */
|
||||||
|
public record DeptUserRoleDto(String userId, String userNm, List<String> roleIds) {}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 다중삭제 공통 본문: { ids: string[] } */
|
||||||
|
public record IdsRequest(@NotEmpty(message = "ids는 필수입니다.") List<String> ids) {}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** { menuId, menuNm, parentMenuId, programId, menuUrl, sortOrd, useYn } */
|
||||||
|
public record MenuDto(
|
||||||
|
String menuId, String menuNm, String parentMenuId, String programId,
|
||||||
|
String menuUrl, Integer sortOrd, String useYn) {}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/** { menuId, menuNm, parentMenuId, programId, menuUrl, sortOrd, useYn } */
|
||||||
|
public record MenuSaveDto(
|
||||||
|
String menuId,
|
||||||
|
@NotBlank(message = "menuNm은 필수입니다.") String menuNm,
|
||||||
|
String parentMenuId, String programId, String menuUrl, Integer sortOrd,
|
||||||
|
@NotBlank(message = "useYn은 필수입니다.") String useYn) {}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UIWS system 이식용 페이지 응답 봉투. 원본 com.urp.uiws.common.response.PageResponse 대체.
|
||||||
|
* MyBatis 기반(Spring Data Page 부재)이므로 content + total + page + size 를 직접 담는다.
|
||||||
|
*/
|
||||||
|
public record PageResponse<T>(
|
||||||
|
List<T> content,
|
||||||
|
long total,
|
||||||
|
int page,
|
||||||
|
int size
|
||||||
|
) {
|
||||||
|
public static <T> PageResponse<T> of(List<T> content, long total, int page, int size) {
|
||||||
|
return new PageResponse<>(content, total, page, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** { programId, programNm, programType, programUrl, category, useYn } */
|
||||||
|
public record ProgramDto(
|
||||||
|
String programId, String programNm, String programType,
|
||||||
|
String programUrl, String category, String useYn) {}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/** { programId, programNm, programType, programUrl, category, useYn } */
|
||||||
|
public record ProgramSaveDto(
|
||||||
|
@NotBlank(message = "programId는 필수입니다.") String programId,
|
||||||
|
@NotBlank(message = "programNm은 필수입니다.") String programNm,
|
||||||
|
@NotBlank(message = "programType은 필수입니다.") String programType,
|
||||||
|
String programUrl, String category,
|
||||||
|
@NotBlank(message = "useYn은 필수입니다.") String useYn) {}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** 가입 화면 공개 조회용 거래처: { companyId, companyNm } */
|
||||||
|
public record PublicCompanyDto(String companyId, String companyNm) {}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** 가입 화면 공개 조회용 부서: { deptId, deptNm } */
|
||||||
|
public record PublicDeptDto(String deptId, String deptNm) {}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** { roleId, roleNm, roleDesc, useYn } */
|
||||||
|
public record RoleDto(String roleId, String roleNm, String roleDesc, String useYn) {}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 부서권한 부여/삭제 본문: { roleIds: string[] } */
|
||||||
|
public record RoleIdsRequest(@NotEmpty(message = "roleIds는 필수입니다.") List<String> roleIds) {}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** { menuId, menuNm, readYn, writeYn } */
|
||||||
|
public record RoleMenuDto(String menuId, String menuNm, String readYn, String writeYn) {}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 권한별 메뉴 매핑 저장 본문: { menus: RoleMenuDto[] } */
|
||||||
|
public record RoleMenuSaveRequest(List<RoleMenuDto> menus) {}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/** { roleId, roleNm, roleDesc, useYn } */
|
||||||
|
public record RoleSaveDto(
|
||||||
|
String roleId,
|
||||||
|
@NotBlank(message = "roleNm은 필수입니다.") String roleNm,
|
||||||
|
String roleDesc,
|
||||||
|
@NotBlank(message = "useYn은 필수입니다.") String useYn) {}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** 사용자 조회 DTO(비밀번호 제외 — 절대 노출 금지). */
|
||||||
|
public record UserDto(
|
||||||
|
String userId, String userNm, String email, String gradeCd,
|
||||||
|
String deptId, String deptNm, String companyId, String companyNm,
|
||||||
|
String roleCd, String naverworksId, String lockYn, String useYn, String approvalYn) {}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/** roleCd = USER/MANAGER/ADMIN (null → USER). 시스템관리(ADMIN 전용)에서만 설정. */
|
||||||
|
public record UserSaveDto(
|
||||||
|
@NotBlank(message = "userId는 필수입니다.") String userId,
|
||||||
|
@NotBlank(message = "userNm은 필수입니다.") String userNm,
|
||||||
|
String password,
|
||||||
|
@NotBlank(message = "email은 필수입니다.") @Email(message = "email 형식이 올바르지 않습니다.") String email,
|
||||||
|
String gradeCd, String deptId, String companyId, String roleCd, String naverworksId,
|
||||||
|
@NotBlank(message = "useYn은 필수입니다.") String useYn) {}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.system.model.*;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 공통코드 그룹/값(tb_uiws_code_grp, tb_uiws_code) 매퍼. 원본 CodeGrpRepository/CodeRepository 변환. */
|
||||||
|
@Mapper
|
||||||
|
public interface CodeMapper {
|
||||||
|
List<SysCodeGrp> searchGroups(@Param("keyword") String keyword,
|
||||||
|
@Param("offset") int offset, @Param("size") int size);
|
||||||
|
long countGroups(@Param("keyword") String keyword);
|
||||||
|
SysCodeGrp findGroupById(@Param("grpCd") String grpCd);
|
||||||
|
boolean groupExists(@Param("grpCd") String grpCd);
|
||||||
|
int insertGroup(SysCodeGrp grp);
|
||||||
|
int updateGroup(SysCodeGrp grp);
|
||||||
|
int deleteGroup(@Param("grpCd") String grpCd);
|
||||||
|
List<SysCode> findValuesByGrp(@Param("grpCd") String grpCd);
|
||||||
|
List<SysCode> findActiveValuesByGrp(@Param("grpCd") String grpCd);
|
||||||
|
int insertValue(SysCode code);
|
||||||
|
int deleteValuesByGrp(@Param("grpCd") String grpCd);
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.system.model.*;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 거래처(tb_uiws_company) 매퍼. 원본 CompanyRepository 변환. */
|
||||||
|
@Mapper
|
||||||
|
public interface CompanyMapper {
|
||||||
|
List<SysCompany> search(@Param("keyword") String keyword,
|
||||||
|
@Param("offset") int offset, @Param("size") int size);
|
||||||
|
long countSearch(@Param("keyword") String keyword);
|
||||||
|
List<SysCompany> searchActive(@Param("keyword") String keyword);
|
||||||
|
List<SysCompany> findActiveOrdered();
|
||||||
|
List<SysCompany> findAll();
|
||||||
|
SysCompany findById(@Param("companyId") String companyId);
|
||||||
|
boolean existsById(@Param("companyId") String companyId);
|
||||||
|
int insert(SysCompany company);
|
||||||
|
int update(SysCompany company);
|
||||||
|
int deleteById(@Param("companyId") String companyId);
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.system.model.*;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 부서(tb_uiws_dept) 매퍼. 원본 DeptRepository 변환. */
|
||||||
|
@Mapper
|
||||||
|
public interface DeptMapper {
|
||||||
|
List<SysDept> search(@Param("keyword") String keyword,
|
||||||
|
@Param("offset") int offset, @Param("size") int size);
|
||||||
|
long countSearch(@Param("keyword") String keyword);
|
||||||
|
List<SysDept> searchActive(@Param("keyword") String keyword);
|
||||||
|
List<SysDept> findActiveOrdered();
|
||||||
|
List<SysDept> findAll();
|
||||||
|
SysDept findById(@Param("deptId") String deptId);
|
||||||
|
boolean existsById(@Param("deptId") String deptId);
|
||||||
|
boolean existsByParentDeptId(@Param("parentDeptId") String parentDeptId);
|
||||||
|
int insert(SysDept dept);
|
||||||
|
int update(SysDept dept);
|
||||||
|
int deleteById(@Param("deptId") String deptId);
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.system.model.*;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 부서-권한 매핑(tb_uiws_dept_role) 매퍼. 원본 DeptRoleRepository 변환. */
|
||||||
|
@Mapper
|
||||||
|
public interface DeptRoleMapper {
|
||||||
|
List<String> findRoleIdsByDeptId(@Param("deptId") String deptId);
|
||||||
|
boolean exists(@Param("deptId") String deptId, @Param("roleId") String roleId);
|
||||||
|
boolean existsByRoleId(@Param("roleId") String roleId);
|
||||||
|
int insert(@Param("deptId") String deptId, @Param("roleId") String roleId, @Param("actor") String actor);
|
||||||
|
int delete(@Param("deptId") String deptId, @Param("roleId") String roleId);
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.system.model.*;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 메뉴(tb_uiws_menu) 매퍼. 원본 SysMenuRepository 변환. */
|
||||||
|
@Mapper
|
||||||
|
public interface MenuMapper {
|
||||||
|
List<SysMenu> searchAll(@Param("offset") int offset, @Param("size") int size);
|
||||||
|
long countAll();
|
||||||
|
List<SysMenu> search(@Param("keyword") String keyword);
|
||||||
|
List<SysMenu> findAllOrdered();
|
||||||
|
SysMenu findById(@Param("menuId") String menuId);
|
||||||
|
boolean existsById(@Param("menuId") String menuId);
|
||||||
|
boolean existsByParentMenuId(@Param("parentMenuId") String parentMenuId);
|
||||||
|
boolean existsByProgramId(@Param("programId") String programId);
|
||||||
|
int insert(SysMenu menu);
|
||||||
|
int update(SysMenu menu);
|
||||||
|
int deleteById(@Param("menuId") String menuId);
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.system.model.*;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 프로그램(tb_uiws_program) 매퍼. 원본 ProgramRepository 변환. */
|
||||||
|
@Mapper
|
||||||
|
public interface ProgramMapper {
|
||||||
|
List<SysProgram> search(@Param("keyword") String keyword, @Param("programType") String programType,
|
||||||
|
@Param("offset") int offset, @Param("size") int size);
|
||||||
|
long countSearch(@Param("keyword") String keyword, @Param("programType") String programType);
|
||||||
|
List<SysProgram> searchActive(@Param("keyword") String keyword);
|
||||||
|
SysProgram findById(@Param("programId") String programId);
|
||||||
|
boolean existsById(@Param("programId") String programId);
|
||||||
|
int insert(SysProgram program);
|
||||||
|
int update(SysProgram program);
|
||||||
|
int deleteById(@Param("programId") String programId);
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.system.model.*;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 권한(tb_uiws_role) 매퍼. 원본 RoleRepository 변환. */
|
||||||
|
@Mapper
|
||||||
|
public interface RoleMapper {
|
||||||
|
List<SysRole> search(@Param("keyword") String keyword,
|
||||||
|
@Param("offset") int offset, @Param("size") int size);
|
||||||
|
long countSearch(@Param("keyword") String keyword);
|
||||||
|
SysRole findById(@Param("roleId") String roleId);
|
||||||
|
boolean existsById(@Param("roleId") String roleId);
|
||||||
|
int insert(SysRole role);
|
||||||
|
int update(SysRole role);
|
||||||
|
int deleteById(@Param("roleId") String roleId);
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.system.model.*;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 권한-메뉴 매핑(tb_uiws_role_menu) 매퍼. 원본 RoleMenuRepository 변환. */
|
||||||
|
@Mapper
|
||||||
|
public interface RoleMenuMapper {
|
||||||
|
List<SysRoleMenu> findByRoleId(@Param("roleId") String roleId);
|
||||||
|
boolean existsByMenuId(@Param("menuId") String menuId);
|
||||||
|
int insert(SysRoleMenu rm);
|
||||||
|
int deleteByRoleId(@Param("roleId") String roleId);
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.system.model.*;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 업무 사용자(tb_uiws_sys_user) 매퍼. 원본 SysUserRepository 변환. */
|
||||||
|
@Mapper
|
||||||
|
public interface SysUserMapper {
|
||||||
|
List<SysUser> search(@Param("keyword") String keyword, @Param("deptId") String deptId,
|
||||||
|
@Param("offset") int offset, @Param("size") int size);
|
||||||
|
long countSearch(@Param("keyword") String keyword, @Param("deptId") String deptId);
|
||||||
|
List<SysUser> searchActive(@Param("keyword") String keyword, @Param("deptId") String deptId);
|
||||||
|
List<SysUser> findByDeptId(@Param("deptId") String deptId);
|
||||||
|
SysUser findById(@Param("userId") String userId);
|
||||||
|
boolean existsById(@Param("userId") String userId);
|
||||||
|
int insert(SysUser user);
|
||||||
|
int update(SysUser user);
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 공통코드 값 (tb_uiws_code, 복합 PK grp_cd+code_val). 원본 com.urp.uiws.domain.Code 이식. */
|
||||||
|
@Data
|
||||||
|
public class SysCode {
|
||||||
|
private String grpCd;
|
||||||
|
private String codeVal;
|
||||||
|
private String codeNm;
|
||||||
|
private Integer sortOrd;
|
||||||
|
private String useYn;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 공통코드 그룹 (tb_uiws_code_grp). 원본 com.urp.uiws.domain.CodeGrp 이식. */
|
||||||
|
@Data
|
||||||
|
public class SysCodeGrp {
|
||||||
|
private String grpCd;
|
||||||
|
private String grpNm;
|
||||||
|
private String useYn;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 거래처(=근무처) (tb_uiws_company). 원본 com.urp.uiws.domain.Company 이식. */
|
||||||
|
@Data
|
||||||
|
public class SysCompany {
|
||||||
|
private String companyId;
|
||||||
|
private String companyNm;
|
||||||
|
private String bizNo;
|
||||||
|
private String useYn;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 부서 (tb_uiws_dept). 원본 com.urp.uiws.domain.Dept 이식. */
|
||||||
|
@Data
|
||||||
|
public class SysDept {
|
||||||
|
private String deptId;
|
||||||
|
private String deptNm;
|
||||||
|
private String parentDeptId;
|
||||||
|
private Integer sortOrd;
|
||||||
|
private String useYn;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 메뉴 (tb_uiws_menu, 2-depth 자기참조). 원본 com.urp.uiws.domain.Menu 이식. */
|
||||||
|
@Data
|
||||||
|
public class SysMenu {
|
||||||
|
private String menuId;
|
||||||
|
private String menuNm;
|
||||||
|
private String parentMenuId;
|
||||||
|
private String programId;
|
||||||
|
private String menuUrl;
|
||||||
|
private Integer sortOrd;
|
||||||
|
private String useYn;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 프로그램(화면) (tb_uiws_program). 원본 com.urp.uiws.domain.Program 이식. */
|
||||||
|
@Data
|
||||||
|
public class SysProgram {
|
||||||
|
private String programId;
|
||||||
|
private String programNm;
|
||||||
|
private String programType; // FORM | POPUP
|
||||||
|
private String programUrl;
|
||||||
|
private String category;
|
||||||
|
private String useYn;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 권한(역할) (tb_uiws_role). 원본 com.urp.uiws.domain.Role 이식. */
|
||||||
|
@Data
|
||||||
|
public class SysRole {
|
||||||
|
private String roleId;
|
||||||
|
private String roleNm;
|
||||||
|
private String roleDesc;
|
||||||
|
private String useYn;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 권한-메뉴 매핑 (tb_uiws_role_menu, 복합 PK). 원본 com.urp.uiws.domain.RoleMenu 이식. */
|
||||||
|
@Data
|
||||||
|
public class SysRoleMenu {
|
||||||
|
private String roleId;
|
||||||
|
private String menuId;
|
||||||
|
private String readYn;
|
||||||
|
private String writeYn;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 업무 사용자 (tb_uiws_sys_user). 원본 com.urp.uiws.domain.User 이식. password 는 BCrypt. */
|
||||||
|
@Data
|
||||||
|
public class SysUser {
|
||||||
|
private String userId;
|
||||||
|
private String userNm;
|
||||||
|
private String password;
|
||||||
|
private String email;
|
||||||
|
private String gradeCd;
|
||||||
|
private String deptId;
|
||||||
|
private String companyId;
|
||||||
|
private String roleCd; // USER | MANAGER | ADMIN
|
||||||
|
private String naverworksId;
|
||||||
|
private Integer loginFailCnt;
|
||||||
|
private String lockYn;
|
||||||
|
private String useYn;
|
||||||
|
private String approvalYn;
|
||||||
|
private String verifyMethod;
|
||||||
|
private String otpSecret;
|
||||||
|
private String pwChangeYn;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,119 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.service;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsApiException;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsErrorCode;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CodeGrpDetailDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CodeGrpDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CodeGrpSaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CodeValueDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.CodeMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.model.SysCode;
|
||||||
|
import com.zioinfo.esn.uiws.system.model.SysCodeGrp;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2.7 코드(codes). 그룹 + 값 복합 관리. 그룹 수정/등록 시 값 목록 전체 치환.
|
||||||
|
* 사용중(use_yn='Y') 코드값이 있으면 그룹 삭제 차단.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CodeService {
|
||||||
|
|
||||||
|
private final CodeMapper codeMapper;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PageResponse<CodeGrpDto> listGroups(String keyword, int page, int size) {
|
||||||
|
List<CodeGrpDto> content = codeMapper.searchGroups(keyword, page * size, size).stream()
|
||||||
|
.map(g -> new CodeGrpDto(g.getGrpCd(), g.getGrpNm(), g.getUseYn())).toList();
|
||||||
|
return PageResponse.of(content, codeMapper.countGroups(keyword), page, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public CodeGrpDetailDto getGroup(String grpCd) {
|
||||||
|
return toDetail(findGrp(grpCd));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<CodeValueDto> getValues(String grpCd) {
|
||||||
|
return codeMapper.findActiveValuesByGrp(grpCd).stream().map(this::toValue).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public CodeGrpDetailDto create(CodeGrpSaveDto dto) {
|
||||||
|
if (codeMapper.groupExists(dto.grpCd())) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.DUPLICATE_KEY, "이미 존재하는 코드그룹입니다.");
|
||||||
|
}
|
||||||
|
SysCodeGrp grp = new SysCodeGrp();
|
||||||
|
grp.setGrpCd(dto.grpCd());
|
||||||
|
grp.setGrpNm(dto.grpNm());
|
||||||
|
grp.setUseYn(dto.useYn());
|
||||||
|
grp.setCreatedBy(SysActor.id());
|
||||||
|
codeMapper.insertGroup(grp);
|
||||||
|
replaceValues(dto.grpCd(), dto.values());
|
||||||
|
return toDetail(findGrp(dto.grpCd()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public CodeGrpDetailDto update(String grpCd, CodeGrpSaveDto dto) {
|
||||||
|
SysCodeGrp grp = findGrp(grpCd);
|
||||||
|
grp.setGrpNm(dto.grpNm());
|
||||||
|
grp.setUseYn(dto.useYn());
|
||||||
|
grp.setUpdatedBy(SysActor.id());
|
||||||
|
codeMapper.updateGroup(grp);
|
||||||
|
codeMapper.deleteValuesByGrp(grpCd);
|
||||||
|
replaceValues(grpCd, dto.values());
|
||||||
|
return toDetail(findGrp(grpCd));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(String grpCd) {
|
||||||
|
findGrp(grpCd);
|
||||||
|
boolean inUse = codeMapper.findValuesByGrp(grpCd).stream().anyMatch(c -> "Y".equals(c.getUseYn()));
|
||||||
|
if (inUse) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.RESOURCE_IN_USE,
|
||||||
|
"사용중인 코드값이 존재하여 코드그룹을 삭제할 수 없습니다.");
|
||||||
|
}
|
||||||
|
codeMapper.deleteValuesByGrp(grpCd);
|
||||||
|
codeMapper.deleteGroup(grpCd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void replaceValues(String grpCd, List<CodeValueDto> values) {
|
||||||
|
if (values == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String actor = SysActor.id();
|
||||||
|
for (CodeValueDto v : values) {
|
||||||
|
SysCode code = new SysCode();
|
||||||
|
code.setGrpCd(grpCd);
|
||||||
|
code.setCodeVal(v.codeVal());
|
||||||
|
code.setCodeNm(v.codeNm());
|
||||||
|
code.setSortOrd(v.sortOrd() == null ? 0 : v.sortOrd());
|
||||||
|
code.setUseYn(v.useYn() == null ? "Y" : v.useYn());
|
||||||
|
code.setCreatedBy(actor);
|
||||||
|
codeMapper.insertValue(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CodeGrpDetailDto toDetail(SysCodeGrp grp) {
|
||||||
|
List<CodeValueDto> values = codeMapper.findValuesByGrp(grp.getGrpCd()).stream().map(this::toValue).toList();
|
||||||
|
return new CodeGrpDetailDto(grp.getGrpCd(), grp.getGrpNm(), grp.getUseYn(), values);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CodeValueDto toValue(SysCode c) {
|
||||||
|
return new CodeValueDto(c.getGrpCd(), c.getCodeVal(), c.getCodeNm(),
|
||||||
|
c.getSortOrd() == null ? 0 : c.getSortOrd(), c.getUseYn());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysCodeGrp findGrp(String grpCd) {
|
||||||
|
SysCodeGrp g = codeMapper.findGroupById(grpCd);
|
||||||
|
if (g == null) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.CODE_GRP_NOT_FOUND);
|
||||||
|
}
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.service;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsApiException;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsErrorCode;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CompanyDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CompanySaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.CompanyMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.model.SysCompany;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 2.4 거래처(companies) CRUD + 검색팝업 + 다중삭제. */
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CompanyService {
|
||||||
|
|
||||||
|
private final CompanyMapper companyMapper;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PageResponse<CompanyDto> list(String keyword, int page, int size) {
|
||||||
|
List<CompanyDto> content = companyMapper.search(keyword, page * size, size).stream().map(this::toDto).toList();
|
||||||
|
return PageResponse.of(content, companyMapper.countSearch(keyword), page, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<CompanyDto> searchPopup(String keyword) {
|
||||||
|
return companyMapper.searchActive(keyword).stream().map(this::toDto).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public CompanyDto get(String companyId) {
|
||||||
|
return toDto(find(companyId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public CompanyDto create(CompanySaveDto dto) {
|
||||||
|
if (dto.companyId() == null || dto.companyId().isBlank()) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "companyId는 등록 시 필수입니다.");
|
||||||
|
}
|
||||||
|
if (companyMapper.existsById(dto.companyId())) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.DUPLICATE_KEY, "이미 존재하는 거래처 ID입니다.");
|
||||||
|
}
|
||||||
|
SysCompany c = new SysCompany();
|
||||||
|
c.setCompanyId(dto.companyId());
|
||||||
|
c.setCompanyNm(dto.companyNm());
|
||||||
|
c.setBizNo(dto.bizNo());
|
||||||
|
c.setUseYn(dto.useYn());
|
||||||
|
c.setCreatedBy(SysActor.id());
|
||||||
|
companyMapper.insert(c);
|
||||||
|
return toDto(find(dto.companyId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public CompanyDto update(String companyId, CompanySaveDto dto) {
|
||||||
|
SysCompany c = find(companyId);
|
||||||
|
c.setCompanyNm(dto.companyNm());
|
||||||
|
c.setBizNo(dto.bizNo());
|
||||||
|
c.setUseYn(dto.useYn());
|
||||||
|
c.setUpdatedBy(SysActor.id());
|
||||||
|
companyMapper.update(c);
|
||||||
|
return toDto(find(companyId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(List<String> ids) {
|
||||||
|
for (String id : ids) {
|
||||||
|
if (!companyMapper.existsById(id)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.COMPANY_NOT_FOUND);
|
||||||
|
}
|
||||||
|
companyMapper.deleteById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompanyDto toDto(SysCompany c) {
|
||||||
|
return new CompanyDto(c.getCompanyId(), c.getCompanyNm(), c.getBizNo(), c.getUseYn());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysCompany find(String companyId) {
|
||||||
|
SysCompany c = companyMapper.findById(companyId);
|
||||||
|
if (c == null) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.COMPANY_NOT_FOUND);
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.service;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsApiException;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsErrorCode;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.DeptUserRoleDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.DeptMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.DeptRoleMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.SysUserMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.model.SysUser;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2.2 부서권한(dept-role). 부서 단위 권한 부여/삭제.
|
||||||
|
* 조회는 부서 소속 사용자 목록에 부서-권한(roleIds)을 동일하게 부여(권한이 부서로 결정되는 모델).
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DeptRoleService {
|
||||||
|
|
||||||
|
private final DeptRoleMapper deptRoleMapper;
|
||||||
|
private final DeptMapper deptMapper;
|
||||||
|
private final SysUserMapper userMapper;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PageResponse<DeptUserRoleDto> listDeptUsers(String deptId, int page, int size) {
|
||||||
|
ensureDept(deptId);
|
||||||
|
List<String> roleIds = deptRoleMapper.findRoleIdsByDeptId(deptId);
|
||||||
|
List<SysUser> users = userMapper.search(null, deptId, page * size, size);
|
||||||
|
List<DeptUserRoleDto> content = users.stream()
|
||||||
|
.map(u -> new DeptUserRoleDto(u.getUserId(), u.getUserNm(), roleIds)).toList();
|
||||||
|
return PageResponse.of(content, userMapper.countSearch(null, deptId), page, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void grant(String deptId, List<String> roleIds) {
|
||||||
|
ensureDept(deptId);
|
||||||
|
String actor = SysActor.id();
|
||||||
|
for (String roleId : roleIds) {
|
||||||
|
deptRoleMapper.insert(deptId, roleId, actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void revoke(String deptId, List<String> roleIds) {
|
||||||
|
ensureDept(deptId);
|
||||||
|
for (String roleId : roleIds) {
|
||||||
|
deptRoleMapper.delete(deptId, roleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureDept(String deptId) {
|
||||||
|
if (!deptMapper.existsById(deptId)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.DEPT_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,181 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.service;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsApiException;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsErrorCode;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.DeptDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.DeptSaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.DeptTreeDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.DeptMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.SysUserMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.model.SysDept;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/** 2.3 부서(depts) CRUD + 검색팝업 + 계층 트리. */
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DeptService {
|
||||||
|
|
||||||
|
private final DeptMapper deptMapper;
|
||||||
|
private final SysUserMapper userMapper;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PageResponse<DeptDto> list(String keyword, int page, int size) {
|
||||||
|
List<DeptDto> content = deptMapper.search(keyword, page * size, size).stream().map(this::toDto).toList();
|
||||||
|
return PageResponse.of(content, deptMapper.countSearch(keyword), page, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<DeptDto> searchPopup(String keyword) {
|
||||||
|
return deptMapper.searchActive(keyword).stream().map(this::toDto).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 부서 계층 트리(루트부터 중첩). sortOrd→deptId 순. */
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<DeptTreeDto> tree() {
|
||||||
|
List<SysDept> all = deptMapper.findAll();
|
||||||
|
Set<String> ids = all.stream().map(SysDept::getDeptId).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
Map<String, List<SysDept>> childrenOf = new HashMap<>();
|
||||||
|
for (SysDept d : all) {
|
||||||
|
if (d.getParentDeptId() != null && ids.contains(d.getParentDeptId())) {
|
||||||
|
childrenOf.computeIfAbsent(d.getParentDeptId(), k -> new ArrayList<>()).add(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Comparator<SysDept> order = Comparator
|
||||||
|
.comparing((SysDept d) -> d.getSortOrd() == null ? 0 : d.getSortOrd())
|
||||||
|
.thenComparing(SysDept::getDeptId);
|
||||||
|
|
||||||
|
List<SysDept> roots = all.stream()
|
||||||
|
.filter(d -> d.getParentDeptId() == null || !ids.contains(d.getParentDeptId()))
|
||||||
|
.sorted(order)
|
||||||
|
.toList();
|
||||||
|
return roots.stream().map(r -> toNode(r, childrenOf, order, new HashSet<>())).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private DeptTreeDto toNode(SysDept d, Map<String, List<SysDept>> childrenOf,
|
||||||
|
Comparator<SysDept> order, Set<String> visited) {
|
||||||
|
if (!visited.add(d.getDeptId())) {
|
||||||
|
return new DeptTreeDto(d.getDeptId(), d.getDeptNm(), d.getParentDeptId(),
|
||||||
|
d.getSortOrd() == null ? 0 : d.getSortOrd(), d.getUseYn(), List.of());
|
||||||
|
}
|
||||||
|
List<DeptTreeDto> children = childrenOf.getOrDefault(d.getDeptId(), List.of()).stream()
|
||||||
|
.sorted(order)
|
||||||
|
.map(c -> toNode(c, childrenOf, order, visited))
|
||||||
|
.toList();
|
||||||
|
return new DeptTreeDto(d.getDeptId(), d.getDeptNm(), d.getParentDeptId(),
|
||||||
|
d.getSortOrd() == null ? 0 : d.getSortOrd(), d.getUseYn(), children);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public DeptDto get(String deptId) {
|
||||||
|
return toDto(find(deptId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public DeptDto create(DeptSaveDto dto) {
|
||||||
|
if (dto.deptId() == null || dto.deptId().isBlank()) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "deptId는 등록 시 필수입니다.");
|
||||||
|
}
|
||||||
|
if (deptMapper.existsById(dto.deptId())) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.DUPLICATE_KEY, "이미 존재하는 부서 ID입니다.");
|
||||||
|
}
|
||||||
|
validateParent(dto.deptId(), dto.parentDeptId());
|
||||||
|
SysDept d = new SysDept();
|
||||||
|
d.setDeptId(dto.deptId());
|
||||||
|
d.setDeptNm(dto.deptNm());
|
||||||
|
d.setParentDeptId(dto.parentDeptId());
|
||||||
|
d.setSortOrd(dto.sortOrd() == null ? 0 : dto.sortOrd());
|
||||||
|
d.setUseYn(dto.useYn());
|
||||||
|
d.setCreatedBy(SysActor.id());
|
||||||
|
deptMapper.insert(d);
|
||||||
|
return toDto(find(dto.deptId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public DeptDto update(String deptId, DeptSaveDto dto) {
|
||||||
|
SysDept d = find(deptId);
|
||||||
|
validateParent(deptId, dto.parentDeptId());
|
||||||
|
d.setDeptNm(dto.deptNm());
|
||||||
|
d.setParentDeptId(dto.parentDeptId());
|
||||||
|
d.setSortOrd(dto.sortOrd() == null ? 0 : dto.sortOrd());
|
||||||
|
d.setUseYn(dto.useYn());
|
||||||
|
d.setUpdatedBy(SysActor.id());
|
||||||
|
deptMapper.update(d);
|
||||||
|
return toDto(find(deptId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(String deptId) {
|
||||||
|
find(deptId);
|
||||||
|
if (deptMapper.existsByParentDeptId(deptId)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.RESOURCE_IN_USE, "하위 부서가 존재하여 삭제할 수 없습니다.");
|
||||||
|
}
|
||||||
|
if (!userMapper.findByDeptId(deptId).isEmpty()) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.RESOURCE_IN_USE, "소속 사용자가 존재하여 삭제할 수 없습니다.");
|
||||||
|
}
|
||||||
|
deptMapper.deleteById(deptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 상위부서 무결성: 자기참조 금지·존재 확인·순환(자기 하위부서를 상위로) 금지. */
|
||||||
|
private void validateParent(String deptId, String parentId) {
|
||||||
|
if (parentId == null || parentId.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (parentId.equals(deptId)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "자기 자신을 상위부서로 지정할 수 없습니다.");
|
||||||
|
}
|
||||||
|
if (!deptMapper.existsById(parentId)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.DEPT_NOT_FOUND, "상위부서가 존재하지 않습니다: " + parentId);
|
||||||
|
}
|
||||||
|
if (deptId != null && selfAndDescendants(deptId).contains(parentId)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "하위부서를 상위부서로 지정할 수 없습니다(순환 구조).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** deptId 자신 + 모든 하위부서 ID 집합(순환 방지용). */
|
||||||
|
private Set<String> selfAndDescendants(String deptId) {
|
||||||
|
List<SysDept> all = deptMapper.findAll();
|
||||||
|
Map<String, List<String>> childrenOf = new HashMap<>();
|
||||||
|
for (SysDept d : all) {
|
||||||
|
if (d.getParentDeptId() != null) {
|
||||||
|
childrenOf.computeIfAbsent(d.getParentDeptId(), k -> new ArrayList<>()).add(d.getDeptId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Set<String> result = new HashSet<>();
|
||||||
|
ArrayList<String> stack = new ArrayList<>();
|
||||||
|
stack.add(deptId);
|
||||||
|
while (!stack.isEmpty()) {
|
||||||
|
String cur = stack.remove(stack.size() - 1);
|
||||||
|
if (!result.add(cur)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
stack.addAll(childrenOf.getOrDefault(cur, List.of()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DeptDto toDto(SysDept d) {
|
||||||
|
return new DeptDto(d.getDeptId(), d.getDeptNm(), d.getParentDeptId(),
|
||||||
|
d.getSortOrd() == null ? 0 : d.getSortOrd(), d.getUseYn());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysDept find(String deptId) {
|
||||||
|
SysDept d = deptMapper.findById(deptId);
|
||||||
|
if (d == null) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.DEPT_NOT_FOUND);
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.service;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsApiException;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsErrorCode;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.MenuDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.MenuSaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.MenuMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.RoleMenuMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.model.SysMenu;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 2.9 메뉴(menus) CRUD + 검색팝업 + 다중삭제. 프론트가 parentMenuId 로 트리 구성. */
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MenuService {
|
||||||
|
|
||||||
|
private final MenuMapper menuMapper;
|
||||||
|
private final RoleMenuMapper roleMenuMapper;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PageResponse<MenuDto> list(int page, int size) {
|
||||||
|
List<MenuDto> content = menuMapper.searchAll(page * size, size).stream().map(this::toDto).toList();
|
||||||
|
return PageResponse.of(content, menuMapper.countAll(), page, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<MenuDto> searchPopup(String keyword) {
|
||||||
|
return menuMapper.search(keyword).stream().map(this::toDto).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public MenuDto get(String menuId) {
|
||||||
|
return toDto(find(menuId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public MenuDto create(MenuSaveDto dto) {
|
||||||
|
if (dto.menuId() == null || dto.menuId().isBlank()) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "menuId는 등록 시 필수입니다.");
|
||||||
|
}
|
||||||
|
if (menuMapper.existsById(dto.menuId())) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.DUPLICATE_KEY, "이미 존재하는 메뉴 ID입니다.");
|
||||||
|
}
|
||||||
|
SysMenu m = new SysMenu();
|
||||||
|
m.setMenuId(dto.menuId());
|
||||||
|
apply(m, dto);
|
||||||
|
m.setCreatedBy(SysActor.id());
|
||||||
|
menuMapper.insert(m);
|
||||||
|
return toDto(find(dto.menuId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public MenuDto update(String menuId, MenuSaveDto dto) {
|
||||||
|
SysMenu m = find(menuId);
|
||||||
|
apply(m, dto);
|
||||||
|
m.setUpdatedBy(SysActor.id());
|
||||||
|
menuMapper.update(m);
|
||||||
|
return toDto(find(menuId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(List<String> ids) {
|
||||||
|
for (String id : ids) {
|
||||||
|
if (!menuMapper.existsById(id)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.MENU_NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (menuMapper.existsByParentMenuId(id)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.RESOURCE_IN_USE, "하위 메뉴가 존재하여 삭제할 수 없습니다: " + id);
|
||||||
|
}
|
||||||
|
if (roleMenuMapper.existsByMenuId(id)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.RESOURCE_IN_USE, "권한에 매핑된 메뉴는 삭제할 수 없습니다: " + id);
|
||||||
|
}
|
||||||
|
menuMapper.deleteById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void apply(SysMenu m, MenuSaveDto dto) {
|
||||||
|
m.setMenuNm(dto.menuNm());
|
||||||
|
m.setParentMenuId(dto.parentMenuId());
|
||||||
|
m.setProgramId(dto.programId());
|
||||||
|
m.setMenuUrl(dto.menuUrl());
|
||||||
|
m.setSortOrd(dto.sortOrd() == null ? 0 : dto.sortOrd());
|
||||||
|
m.setUseYn(dto.useYn());
|
||||||
|
}
|
||||||
|
|
||||||
|
private MenuDto toDto(SysMenu m) {
|
||||||
|
return new MenuDto(m.getMenuId(), m.getMenuNm(), m.getParentMenuId(), m.getProgramId(),
|
||||||
|
m.getMenuUrl(), m.getSortOrd() == null ? 0 : m.getSortOrd(), m.getUseYn());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysMenu find(String menuId) {
|
||||||
|
SysMenu m = menuMapper.findById(menuId);
|
||||||
|
if (m == null) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.MENU_NOT_FOUND);
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.service;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsApiException;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsErrorCode;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.ProgramDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.ProgramSaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.MenuMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.ProgramMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.model.SysProgram;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 2.8 프로그램(programs) CRUD + 검색팝업 + 다중삭제. */
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ProgramService {
|
||||||
|
|
||||||
|
private final ProgramMapper programMapper;
|
||||||
|
private final MenuMapper menuMapper;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PageResponse<ProgramDto> list(String keyword, String programType, int page, int size) {
|
||||||
|
List<ProgramDto> content = programMapper.search(keyword, programType, page * size, size)
|
||||||
|
.stream().map(this::toDto).toList();
|
||||||
|
return PageResponse.of(content, programMapper.countSearch(keyword, programType), page, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<ProgramDto> searchPopup(String keyword) {
|
||||||
|
return programMapper.searchActive(keyword).stream().map(this::toDto).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public ProgramDto get(String programId) {
|
||||||
|
return toDto(find(programId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ProgramDto create(ProgramSaveDto dto) {
|
||||||
|
if (programMapper.existsById(dto.programId())) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.DUPLICATE_KEY, "이미 존재하는 프로그램 ID입니다.");
|
||||||
|
}
|
||||||
|
SysProgram p = new SysProgram();
|
||||||
|
p.setProgramId(dto.programId());
|
||||||
|
apply(p, dto);
|
||||||
|
p.setCreatedBy(SysActor.id());
|
||||||
|
programMapper.insert(p);
|
||||||
|
return toDto(find(dto.programId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ProgramDto update(String programId, ProgramSaveDto dto) {
|
||||||
|
SysProgram p = find(programId);
|
||||||
|
apply(p, dto);
|
||||||
|
p.setUpdatedBy(SysActor.id());
|
||||||
|
programMapper.update(p);
|
||||||
|
return toDto(find(programId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(List<String> ids) {
|
||||||
|
for (String id : ids) {
|
||||||
|
if (!programMapper.existsById(id)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.PROGRAM_NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (menuMapper.existsByProgramId(id)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.RESOURCE_IN_USE,
|
||||||
|
"메뉴에서 참조 중인 프로그램은 삭제할 수 없습니다: " + id);
|
||||||
|
}
|
||||||
|
programMapper.deleteById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void apply(SysProgram p, ProgramSaveDto dto) {
|
||||||
|
p.setProgramNm(dto.programNm());
|
||||||
|
p.setProgramType(dto.programType());
|
||||||
|
p.setProgramUrl(dto.programUrl());
|
||||||
|
p.setCategory(dto.category());
|
||||||
|
p.setUseYn(dto.useYn());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProgramDto toDto(SysProgram p) {
|
||||||
|
return new ProgramDto(p.getProgramId(), p.getProgramNm(), p.getProgramType(),
|
||||||
|
p.getProgramUrl(), p.getCategory(), p.getUseYn());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysProgram find(String programId) {
|
||||||
|
SysProgram p = programMapper.findById(programId);
|
||||||
|
if (p == null) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.PROGRAM_NOT_FOUND);
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.service;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PublicCompanyDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PublicDeptDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.CompanyMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.DeptMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 가입 화면(비인증) 공개 조회. use_yn='Y' 부서/거래처만 노출.
|
||||||
|
* (가입 시 존재하지 않는 FK 입력 → 오류 방지)
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PublicLookupService {
|
||||||
|
|
||||||
|
private final DeptMapper deptMapper;
|
||||||
|
private final CompanyMapper companyMapper;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<PublicDeptDto> depts() {
|
||||||
|
return deptMapper.findActiveOrdered().stream()
|
||||||
|
.map(d -> new PublicDeptDto(d.getDeptId(), d.getDeptNm())).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<PublicCompanyDto> companies() {
|
||||||
|
return companyMapper.findActiveOrdered().stream()
|
||||||
|
.map(c -> new PublicCompanyDto(c.getCompanyId(), c.getCompanyNm())).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.service;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsApiException;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsErrorCode;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.RoleDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.RoleMenuDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.MenuMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.RoleMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.RoleMenuMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.model.SysMenu;
|
||||||
|
import com.zioinfo.esn.uiws.system.model.SysRole;
|
||||||
|
import com.zioinfo.esn.uiws.system.model.SysRoleMenu;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2.5 메뉴생성(role-menus). 권한 목록 + 권한별 메뉴 매핑 조회/저장.
|
||||||
|
* 조회는 전체 사용중 메뉴 기준으로 매핑 여부(read/write)를 합쳐 반환.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RoleMenuService {
|
||||||
|
|
||||||
|
private final RoleMapper roleMapper;
|
||||||
|
private final RoleMenuMapper roleMenuMapper;
|
||||||
|
private final MenuMapper menuMapper;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PageResponse<RoleDto> listRoles(int page, int size) {
|
||||||
|
List<RoleDto> content = roleMapper.search(null, page * size, size).stream()
|
||||||
|
.map(this::toRoleDto).toList();
|
||||||
|
return PageResponse.of(content, roleMapper.countSearch(null), page, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<RoleMenuDto> getRoleMenus(String roleId) {
|
||||||
|
ensureRole(roleId);
|
||||||
|
Map<String, SysRoleMenu> mapped = new LinkedHashMap<>();
|
||||||
|
for (SysRoleMenu rm : roleMenuMapper.findByRoleId(roleId)) {
|
||||||
|
mapped.put(rm.getMenuId(), rm);
|
||||||
|
}
|
||||||
|
List<SysMenu> allMenus = menuMapper.findAllOrdered();
|
||||||
|
return allMenus.stream().map(m -> {
|
||||||
|
SysRoleMenu rm = mapped.get(m.getMenuId());
|
||||||
|
String readYn = rm != null ? rm.getReadYn() : "N";
|
||||||
|
String writeYn = rm != null ? rm.getWriteYn() : "N";
|
||||||
|
return new RoleMenuDto(m.getMenuId(), m.getMenuNm(), readYn, writeYn);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 전체 치환 저장: read/write 중 하나라도 'Y'면 매핑 보존, 둘 다 'N'이면 제거. */
|
||||||
|
@Transactional
|
||||||
|
public void saveRoleMenus(String roleId, List<RoleMenuDto> menus) {
|
||||||
|
ensureRole(roleId);
|
||||||
|
String actor = SysActor.id();
|
||||||
|
roleMenuMapper.deleteByRoleId(roleId);
|
||||||
|
if (menus == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (RoleMenuDto dto : menus) {
|
||||||
|
boolean read = "Y".equals(dto.readYn());
|
||||||
|
boolean write = "Y".equals(dto.writeYn());
|
||||||
|
if (!read && !write) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SysRoleMenu rm = new SysRoleMenu();
|
||||||
|
rm.setRoleId(roleId);
|
||||||
|
rm.setMenuId(dto.menuId());
|
||||||
|
rm.setReadYn(read ? "Y" : "N");
|
||||||
|
rm.setWriteYn(write ? "Y" : "N");
|
||||||
|
rm.setCreatedBy(actor);
|
||||||
|
roleMenuMapper.insert(rm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RoleDto toRoleDto(SysRole r) {
|
||||||
|
return new RoleDto(r.getRoleId(), r.getRoleNm(), r.getRoleDesc(), r.getUseYn());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureRole(String roleId) {
|
||||||
|
if (!roleMapper.existsById(roleId)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.ROLE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.service;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsApiException;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsErrorCode;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.RoleDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.RoleSaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.DeptRoleMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.RoleMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.RoleMenuMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.model.SysRole;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 2.1 권한(roles) CRUD. */
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RoleService {
|
||||||
|
|
||||||
|
private final RoleMapper roleMapper;
|
||||||
|
private final RoleMenuMapper roleMenuMapper;
|
||||||
|
private final DeptRoleMapper deptRoleMapper;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PageResponse<RoleDto> list(String keyword, int page, int size) {
|
||||||
|
List<RoleDto> content = roleMapper.search(keyword, page * size, size).stream().map(this::toDto).toList();
|
||||||
|
return PageResponse.of(content, roleMapper.countSearch(keyword), page, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public RoleDto get(String roleId) {
|
||||||
|
return toDto(find(roleId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public RoleDto create(RoleSaveDto dto) {
|
||||||
|
if (dto.roleId() == null || dto.roleId().isBlank()) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "roleId는 등록 시 필수입니다.");
|
||||||
|
}
|
||||||
|
if (roleMapper.existsById(dto.roleId())) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.DUPLICATE_KEY, "이미 존재하는 권한 ID입니다.");
|
||||||
|
}
|
||||||
|
SysRole role = new SysRole();
|
||||||
|
role.setRoleId(dto.roleId());
|
||||||
|
role.setRoleNm(dto.roleNm());
|
||||||
|
role.setRoleDesc(dto.roleDesc());
|
||||||
|
role.setUseYn(dto.useYn());
|
||||||
|
role.setCreatedBy(SysActor.id());
|
||||||
|
roleMapper.insert(role);
|
||||||
|
return toDto(find(dto.roleId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public RoleDto update(String roleId, RoleSaveDto dto) {
|
||||||
|
SysRole role = find(roleId);
|
||||||
|
role.setRoleNm(dto.roleNm());
|
||||||
|
role.setRoleDesc(dto.roleDesc());
|
||||||
|
role.setUseYn(dto.useYn());
|
||||||
|
role.setUpdatedBy(SysActor.id());
|
||||||
|
roleMapper.update(role);
|
||||||
|
return toDto(find(roleId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(List<String> ids) {
|
||||||
|
for (String id : ids) {
|
||||||
|
if (deptRoleMapper.existsByRoleId(id)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.RESOURCE_IN_USE, "부서에 부여된 권한은 삭제할 수 없습니다: " + id);
|
||||||
|
}
|
||||||
|
roleMenuMapper.deleteByRoleId(id);
|
||||||
|
roleMapper.deleteById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RoleDto toDto(SysRole r) {
|
||||||
|
return new RoleDto(r.getRoleId(), r.getRoleNm(), r.getRoleDesc(), r.getUseYn());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysRole find(String roleId) {
|
||||||
|
SysRole r = roleMapper.findById(roleId);
|
||||||
|
if (r == null) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.ROLE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.service;
|
||||||
|
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 사용자 ID 추출(감사 컬럼 created_by/updated_by 세팅용). 원본 CurrentUser 대체.
|
||||||
|
* CMS JwtFilter 가 username(String) 을 principal 로 설정 → getName() 으로 추출.
|
||||||
|
*/
|
||||||
|
public final class SysActor {
|
||||||
|
|
||||||
|
private static final String SYSTEM = "SYSTEM";
|
||||||
|
|
||||||
|
private SysActor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String id() {
|
||||||
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (auth != null && auth.getName() != null && !auth.getName().isBlank()) {
|
||||||
|
return auth.getName();
|
||||||
|
}
|
||||||
|
return SYSTEM;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,234 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.service;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsApiException;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsErrorCode;
|
||||||
|
import com.zioinfo.esn.uiws.common.mail.MailSender;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CheckIdResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.UserDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.UserSaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.CompanyMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.DeptMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.mapper.SysUserMapper;
|
||||||
|
import com.zioinfo.esn.uiws.system.model.SysCompany;
|
||||||
|
import com.zioinfo.esn.uiws.system.model.SysDept;
|
||||||
|
import com.zioinfo.esn.uiws.system.model.SysUser;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2.6 사용자(users). 관리자 사용자 CRUD/검색/중복확인/다중삭제/비번초기화/잠금해제/승인.
|
||||||
|
* 관리자 등록 사용자는 approval_yn='Y'. 비밀번호는 BCrypt — 응답·로그(코드 외)에 절대 노출 금지.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserService {
|
||||||
|
|
||||||
|
private static final SecureRandom RANDOM = new SecureRandom();
|
||||||
|
|
||||||
|
private final SysUserMapper userMapper;
|
||||||
|
private final DeptMapper deptMapper;
|
||||||
|
private final CompanyMapper companyMapper;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final MailSender mailSender;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PageResponse<UserDto> list(String keyword, String deptId, int page, int size) {
|
||||||
|
Map<String, String> deptNames = deptNameMap();
|
||||||
|
Map<String, String> companyNames = companyNameMap();
|
||||||
|
List<UserDto> content = userMapper.search(keyword, deptId, page * size, size).stream()
|
||||||
|
.map(u -> toDto(u, deptNames, companyNames)).toList();
|
||||||
|
return PageResponse.of(content, userMapper.countSearch(keyword, deptId), page, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<UserDto> searchPopup(String keyword, String deptId) {
|
||||||
|
Map<String, String> deptNames = deptNameMap();
|
||||||
|
Map<String, String> companyNames = companyNameMap();
|
||||||
|
return userMapper.searchActive(keyword, deptId).stream()
|
||||||
|
.map(u -> toDto(u, deptNames, companyNames)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public CheckIdResponse checkId(String userId) {
|
||||||
|
boolean available = userId != null && !userId.isBlank() && !userMapper.existsById(userId);
|
||||||
|
return new CheckIdResponse(available);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public UserDto get(String userId) {
|
||||||
|
return toDto(find(userId), deptNameMap(), companyNameMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public UserDto create(UserSaveDto dto) {
|
||||||
|
if (userMapper.existsById(dto.userId())) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.USER_ID_DUPLICATED);
|
||||||
|
}
|
||||||
|
String rawPw = (dto.password() == null || dto.password().isBlank())
|
||||||
|
? generateTempPassword() : dto.password();
|
||||||
|
|
||||||
|
SysUser u = new SysUser();
|
||||||
|
u.setUserId(dto.userId());
|
||||||
|
u.setUserNm(dto.userNm());
|
||||||
|
u.setPassword(passwordEncoder.encode(rawPw));
|
||||||
|
u.setEmail(dto.email());
|
||||||
|
u.setGradeCd(dto.gradeCd());
|
||||||
|
u.setDeptId(dto.deptId());
|
||||||
|
u.setCompanyId(dto.companyId());
|
||||||
|
u.setRoleCd(normalizeRole(dto.roleCd()));
|
||||||
|
u.setNaverworksId(blankToNull(dto.naverworksId()));
|
||||||
|
u.setLoginFailCnt(0);
|
||||||
|
u.setLockYn("N");
|
||||||
|
u.setUseYn(dto.useYn() == null ? "Y" : dto.useYn());
|
||||||
|
u.setApprovalYn("Y"); // 관리자 생성 → 즉시 승인
|
||||||
|
u.setVerifyMethod("EMAIL");
|
||||||
|
u.setPwChangeYn("Y"); // 최초 로그인 시 비번 변경 유도
|
||||||
|
u.setCreatedBy(SysActor.id());
|
||||||
|
userMapper.insert(u);
|
||||||
|
return toDto(find(dto.userId()), deptNameMap(), companyNameMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public UserDto update(String userId, UserSaveDto dto) {
|
||||||
|
SysUser u = find(userId);
|
||||||
|
u.setUserNm(dto.userNm());
|
||||||
|
u.setEmail(dto.email());
|
||||||
|
u.setGradeCd(dto.gradeCd());
|
||||||
|
u.setDeptId(dto.deptId());
|
||||||
|
u.setCompanyId(dto.companyId());
|
||||||
|
if (dto.roleCd() != null && !dto.roleCd().isBlank()) {
|
||||||
|
u.setRoleCd(normalizeRole(dto.roleCd()));
|
||||||
|
}
|
||||||
|
u.setNaverworksId(blankToNull(dto.naverworksId()));
|
||||||
|
if (dto.useYn() != null && !dto.useYn().isBlank()) {
|
||||||
|
u.setUseYn(dto.useYn());
|
||||||
|
}
|
||||||
|
u.setPassword((dto.password() != null && !dto.password().isBlank())
|
||||||
|
? passwordEncoder.encode(dto.password()) : null); // null → XML 에서 비밀번호 미변경
|
||||||
|
u.setUpdatedBy(SysActor.id());
|
||||||
|
userMapper.update(u);
|
||||||
|
return toDto(find(userId), deptNameMap(), companyNameMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 사용자 삭제 = 소프트삭제(use_yn='N' + 승인 회수). 이력 보존 위해 하드삭제 금지. */
|
||||||
|
@Transactional
|
||||||
|
public void delete(List<String> ids) {
|
||||||
|
for (String id : ids) {
|
||||||
|
SysUser u = find(id);
|
||||||
|
u.setUseYn("N");
|
||||||
|
u.setApprovalYn("N");
|
||||||
|
u.setUpdatedBy(SysActor.id());
|
||||||
|
userMapper.update(u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 비번 초기화: 임시비번 생성 → BCrypt 저장 → 메일/로그(코드 외)로만 전달, 잠금 해제. */
|
||||||
|
@Transactional
|
||||||
|
public void resetPassword(String userId) {
|
||||||
|
SysUser u = find(userId);
|
||||||
|
String temp = generateTempPassword();
|
||||||
|
u.setPassword(passwordEncoder.encode(temp));
|
||||||
|
u.setLockYn("N");
|
||||||
|
u.setLoginFailCnt(0);
|
||||||
|
u.setPwChangeYn("Y");
|
||||||
|
u.setUpdatedBy(SysActor.id());
|
||||||
|
userMapper.update(u);
|
||||||
|
// 임시비번은 메일/로그 채널로만 — API 응답에는 절대 미포함(보안 불변규칙).
|
||||||
|
mailSender.send(u.getEmail(), "[zioinfo-esn] 비밀번호 초기화",
|
||||||
|
String.format("임시 비밀번호: %s\n로그인 후 즉시 변경하세요.", temp));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 잠금 해제. */
|
||||||
|
@Transactional
|
||||||
|
public void unlock(String userId) {
|
||||||
|
SysUser u = find(userId);
|
||||||
|
u.setLockYn("N");
|
||||||
|
u.setLoginFailCnt(0);
|
||||||
|
u.setUpdatedBy(SysActor.id());
|
||||||
|
userMapper.update(u);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 가입 승인 — approval_yn='Y'. */
|
||||||
|
@Transactional
|
||||||
|
public void approve(String userId) {
|
||||||
|
SysUser u = find(userId);
|
||||||
|
u.setApprovalYn("Y");
|
||||||
|
u.setUpdatedBy(SysActor.id());
|
||||||
|
userMapper.update(u);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 승인 취소 — approval_yn='N'. */
|
||||||
|
@Transactional
|
||||||
|
public void revokeApproval(String userId) {
|
||||||
|
SysUser u = find(userId);
|
||||||
|
u.setApprovalYn("N");
|
||||||
|
u.setUpdatedBy(SysActor.id());
|
||||||
|
userMapper.update(u);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
private UserDto toDto(SysUser u, Map<String, String> deptNames, Map<String, String> companyNames) {
|
||||||
|
String deptNm = u.getDeptId() == null ? null : deptNames.get(u.getDeptId());
|
||||||
|
String companyNm = u.getCompanyId() == null ? null : companyNames.get(u.getCompanyId());
|
||||||
|
return new UserDto(u.getUserId(), u.getUserNm(), u.getEmail(), u.getGradeCd(),
|
||||||
|
u.getDeptId(), deptNm, u.getCompanyId(), companyNm,
|
||||||
|
u.getRoleCd(), u.getNaverworksId(), u.getLockYn(), u.getUseYn(), u.getApprovalYn());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String blankToNull(String s) {
|
||||||
|
return (s == null || s.isBlank()) ? null : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeRole(String roleCd) {
|
||||||
|
if ("MANAGER".equals(roleCd) || "ADMIN".equals(roleCd)) {
|
||||||
|
return roleCd;
|
||||||
|
}
|
||||||
|
return "USER";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> deptNameMap() {
|
||||||
|
return deptMapper.findAll().stream()
|
||||||
|
.collect(Collectors.toMap(SysDept::getDeptId, SysDept::getDeptNm, (a, b) -> a, HashMap::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> companyNameMap() {
|
||||||
|
return companyMapper.findAll().stream()
|
||||||
|
.collect(Collectors.toMap(SysCompany::getCompanyId, SysCompany::getCompanyNm, (a, b) -> a, HashMap::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysUser find(String userId) {
|
||||||
|
SysUser u = userMapper.findById(userId);
|
||||||
|
if (u == null) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.USER_NOT_FOUND);
|
||||||
|
}
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateTempPassword() {
|
||||||
|
String upper = "ABCDEFGHJKLMNPQRSTUVWXYZ";
|
||||||
|
String lower = "abcdefghijkmnpqrstuvwxyz";
|
||||||
|
String digit = "23456789";
|
||||||
|
String special = "!@#$%";
|
||||||
|
String all = upper + lower + digit + special;
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(upper.charAt(RANDOM.nextInt(upper.length())));
|
||||||
|
sb.append(lower.charAt(RANDOM.nextInt(lower.length())));
|
||||||
|
sb.append(digit.charAt(RANDOM.nextInt(digit.length())));
|
||||||
|
sb.append(special.charAt(RANDOM.nextInt(special.length())));
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
sb.append(all.charAt(RANDOM.nextInt(all.length())));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -21,9 +21,13 @@ spring:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
schema-locations:
|
schema-locations:
|
||||||
- classpath:db/schema.sql
|
- classpath:db/schema.sql
|
||||||
|
# UIWS system(권한관리) 이식 — tb_uiws_* (멱등, 90 → 91 순서)
|
||||||
|
- classpath:db/90_uiws_system.sql
|
||||||
- classpath:db/91_uiws_port.sql
|
- classpath:db/91_uiws_port.sql
|
||||||
# AI 기법(중앙 guardia-rag) 토글 — 테넌트 격리 esn_rag_setting (멱등)
|
# AI 기법(중앙 guardia-rag) 토글 — 테넌트 격리 esn_rag_setting (멱등)
|
||||||
- classpath:db/92_esn_rag_setting.sql
|
- classpath:db/92_esn_rag_setting.sql
|
||||||
|
# 로그인 보조(가입 승인대기/아이디찾기) — esn_user name/approval_status ALTER (멱등)
|
||||||
|
- classpath:db/93_esn_login_helper.sql
|
||||||
web:
|
web:
|
||||||
resources:
|
resources:
|
||||||
static-locations: classpath:/static/
|
static-locations: classpath:/static/
|
||||||
|
|||||||
240
backend/src/main/resources/db/90_uiws_system.sql
Normal file
240
backend/src/main/resources/db/90_uiws_system.sql
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- UIWS system(시스템관리·권한관리) 이식 (zioinfo-esn) — com.zioinfo.esn.uiws.system
|
||||||
|
-- 원본: workspace/uiws/db/02_schema_core.sql (TB_DEPT/COMPANY/CODE_GRP/CODE/USER/ROLE/
|
||||||
|
-- DEPT_ROLE/PROGRAM/MENU/ROLE_MENU). 멀티테넌트 키 체계(VARCHAR ID)를 그대로 유지.
|
||||||
|
--
|
||||||
|
-- 네임스페이스 격리: 원본 TB_* → 소문자 tb_uiws_ 프리픽스. ESN esn_user(BIGSERIAL) 와
|
||||||
|
-- 별개 계정 모델(tb_uiws_sys_user: VARCHAR user_id) — 절대 병합하지 않는다.
|
||||||
|
-- 멱등: 전부 CREATE TABLE IF NOT EXISTS / ON CONFLICT DO NOTHING. mode:always 재실행 완전 멱등.
|
||||||
|
-- FK 정책: tb_uiws_* 내부 참조만 물리 FK(원본과 동일). 비밀번호는 BCrypt 저장.
|
||||||
|
-- 보안: 비밀번호/임시비번/자격증명은 응답·로그(코드 외)로 노출하지 않는다(불변규칙).
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
SET client_encoding = 'UTF8';
|
||||||
|
|
||||||
|
-- ───────────────────────────────────────────────────────────────────────────
|
||||||
|
-- 부서 (자기참조 계층)
|
||||||
|
-- ───────────────────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS tb_uiws_dept (
|
||||||
|
dept_id VARCHAR(20) NOT NULL,
|
||||||
|
dept_nm VARCHAR(100) NOT NULL,
|
||||||
|
parent_dept_id VARCHAR(20),
|
||||||
|
sort_ord INT DEFAULT 0,
|
||||||
|
use_yn CHAR(1) NOT NULL DEFAULT 'Y',
|
||||||
|
created_by VARCHAR(20) NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
updated_by VARCHAR(20),
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
CONSTRAINT pk_tb_uiws_dept PRIMARY KEY (dept_id),
|
||||||
|
CONSTRAINT fk_uiws_dept_parent FOREIGN KEY (parent_dept_id) REFERENCES tb_uiws_dept (dept_id),
|
||||||
|
CONSTRAINT ck_uiws_dept_use_yn CHECK (use_yn IN ('Y','N'))
|
||||||
|
);
|
||||||
|
COMMENT ON TABLE tb_uiws_dept IS 'UIWS 이식: 부서 (2-depth 계층, 자기참조)';
|
||||||
|
|
||||||
|
-- ───────────────────────────────────────────────────────────────────────────
|
||||||
|
-- 거래처(=근무처) 마스터
|
||||||
|
-- ───────────────────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS tb_uiws_company (
|
||||||
|
company_id VARCHAR(20) NOT NULL,
|
||||||
|
company_nm VARCHAR(100) NOT NULL,
|
||||||
|
biz_no VARCHAR(20),
|
||||||
|
use_yn CHAR(1) NOT NULL DEFAULT 'Y',
|
||||||
|
created_by VARCHAR(20) NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
updated_by VARCHAR(20),
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
CONSTRAINT pk_tb_uiws_company PRIMARY KEY (company_id),
|
||||||
|
CONSTRAINT ck_uiws_company_use_yn CHECK (use_yn IN ('Y','N'))
|
||||||
|
);
|
||||||
|
COMMENT ON TABLE tb_uiws_company IS 'UIWS 이식: 거래처=근무처 마스터';
|
||||||
|
|
||||||
|
-- ───────────────────────────────────────────────────────────────────────────
|
||||||
|
-- 공통코드 그룹 / 값 (복합 PK)
|
||||||
|
-- ───────────────────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS tb_uiws_code_grp (
|
||||||
|
grp_cd VARCHAR(30) NOT NULL,
|
||||||
|
grp_nm VARCHAR(100) NOT NULL,
|
||||||
|
use_yn CHAR(1) NOT NULL DEFAULT 'Y',
|
||||||
|
created_by VARCHAR(20) NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
updated_by VARCHAR(20),
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
CONSTRAINT pk_tb_uiws_code_grp PRIMARY KEY (grp_cd),
|
||||||
|
CONSTRAINT ck_uiws_code_grp_use_yn CHECK (use_yn IN ('Y','N'))
|
||||||
|
);
|
||||||
|
COMMENT ON TABLE tb_uiws_code_grp IS 'UIWS 이식: 공통코드 그룹';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tb_uiws_code (
|
||||||
|
grp_cd VARCHAR(30) NOT NULL,
|
||||||
|
code_val VARCHAR(30) NOT NULL,
|
||||||
|
code_nm VARCHAR(100) NOT NULL,
|
||||||
|
sort_ord INT DEFAULT 0,
|
||||||
|
use_yn CHAR(1) NOT NULL DEFAULT 'Y',
|
||||||
|
created_by VARCHAR(20) NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
updated_by VARCHAR(20),
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
CONSTRAINT pk_tb_uiws_code PRIMARY KEY (grp_cd, code_val),
|
||||||
|
CONSTRAINT fk_uiws_code_grp FOREIGN KEY (grp_cd) REFERENCES tb_uiws_code_grp (grp_cd),
|
||||||
|
CONSTRAINT ck_uiws_code_use_yn CHECK (use_yn IN ('Y','N'))
|
||||||
|
);
|
||||||
|
COMMENT ON TABLE tb_uiws_code IS 'UIWS 이식: 공통코드 값 (복합 PK: grp_cd + code_val)';
|
||||||
|
|
||||||
|
-- ───────────────────────────────────────────────────────────────────────────
|
||||||
|
-- 사용자(계정) — CMS cms_user 와 별개(VARCHAR user_id, 멀티테넌트 업무 사용자)
|
||||||
|
-- ───────────────────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS tb_uiws_sys_user (
|
||||||
|
user_id VARCHAR(20) NOT NULL,
|
||||||
|
user_nm VARCHAR(50) NOT NULL,
|
||||||
|
password VARCHAR(100) NOT NULL, -- BCrypt
|
||||||
|
email VARCHAR(100) NOT NULL,
|
||||||
|
grade_cd VARCHAR(20),
|
||||||
|
dept_id VARCHAR(20),
|
||||||
|
company_id VARCHAR(20),
|
||||||
|
role_cd VARCHAR(20) NOT NULL DEFAULT 'USER', -- USER/MANAGER/ADMIN
|
||||||
|
naverworks_id VARCHAR(100),
|
||||||
|
login_fail_cnt INT NOT NULL DEFAULT 0,
|
||||||
|
lock_yn CHAR(1) NOT NULL DEFAULT 'N',
|
||||||
|
use_yn CHAR(1) NOT NULL DEFAULT 'Y',
|
||||||
|
approval_yn CHAR(1) NOT NULL DEFAULT 'N',
|
||||||
|
verify_method VARCHAR(20) NOT NULL DEFAULT 'EMAIL',
|
||||||
|
otp_secret VARCHAR(100),
|
||||||
|
pw_change_yn CHAR(1) NOT NULL DEFAULT 'N',
|
||||||
|
created_by VARCHAR(20) NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
updated_by VARCHAR(20),
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
CONSTRAINT pk_tb_uiws_sys_user PRIMARY KEY (user_id),
|
||||||
|
CONSTRAINT fk_uiws_sysuser_dept FOREIGN KEY (dept_id) REFERENCES tb_uiws_dept (dept_id),
|
||||||
|
CONSTRAINT fk_uiws_sysuser_company FOREIGN KEY (company_id) REFERENCES tb_uiws_company (company_id),
|
||||||
|
CONSTRAINT uq_uiws_sysuser_email UNIQUE (email),
|
||||||
|
CONSTRAINT ck_uiws_sysuser_lock_yn CHECK (lock_yn IN ('Y','N')),
|
||||||
|
CONSTRAINT ck_uiws_sysuser_use_yn CHECK (use_yn IN ('Y','N')),
|
||||||
|
CONSTRAINT ck_uiws_sysuser_approval_yn CHECK (approval_yn IN ('Y','N')),
|
||||||
|
CONSTRAINT ck_uiws_sysuser_verify CHECK (verify_method IN ('EMAIL','OTP')),
|
||||||
|
CONSTRAINT ck_uiws_sysuser_pwchg_yn CHECK (pw_change_yn IN ('Y','N')),
|
||||||
|
CONSTRAINT ck_uiws_sysuser_role_cd CHECK (role_cd IN ('USER','MANAGER','ADMIN')),
|
||||||
|
CONSTRAINT ck_uiws_sysuser_fail_cnt CHECK (login_fail_cnt >= 0)
|
||||||
|
);
|
||||||
|
COMMENT ON TABLE tb_uiws_sys_user IS 'UIWS 이식: 업무 사용자 계정(BCrypt, 잠금/실패횟수, 가입승인) — CMS cms_user 와 별개';
|
||||||
|
|
||||||
|
-- ───────────────────────────────────────────────────────────────────────────
|
||||||
|
-- 권한(역할) / 부서-권한 매핑
|
||||||
|
-- ───────────────────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS tb_uiws_role (
|
||||||
|
role_id VARCHAR(20) NOT NULL,
|
||||||
|
role_nm VARCHAR(100) NOT NULL,
|
||||||
|
role_desc VARCHAR(255),
|
||||||
|
use_yn CHAR(1) NOT NULL DEFAULT 'Y',
|
||||||
|
created_by VARCHAR(20) NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
updated_by VARCHAR(20),
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
CONSTRAINT pk_tb_uiws_role PRIMARY KEY (role_id),
|
||||||
|
CONSTRAINT ck_uiws_role_use_yn CHECK (use_yn IN ('Y','N'))
|
||||||
|
);
|
||||||
|
COMMENT ON TABLE tb_uiws_role IS 'UIWS 이식: 권한(역할)';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tb_uiws_dept_role (
|
||||||
|
dept_id VARCHAR(20) NOT NULL,
|
||||||
|
role_id VARCHAR(20) NOT NULL,
|
||||||
|
created_by VARCHAR(20) NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
updated_by VARCHAR(20),
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
CONSTRAINT pk_tb_uiws_dept_role PRIMARY KEY (dept_id, role_id),
|
||||||
|
CONSTRAINT fk_uiws_deptrole_dept FOREIGN KEY (dept_id) REFERENCES tb_uiws_dept (dept_id),
|
||||||
|
CONSTRAINT fk_uiws_deptrole_role FOREIGN KEY (role_id) REFERENCES tb_uiws_role (role_id)
|
||||||
|
);
|
||||||
|
COMMENT ON TABLE tb_uiws_dept_role IS 'UIWS 이식: 부서-권한 매핑 (복합 PK)';
|
||||||
|
|
||||||
|
-- ───────────────────────────────────────────────────────────────────────────
|
||||||
|
-- 프로그램(화면) / 메뉴 / 권한-메뉴 매핑
|
||||||
|
-- ───────────────────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS tb_uiws_program (
|
||||||
|
program_id VARCHAR(20) NOT NULL,
|
||||||
|
program_nm VARCHAR(100) NOT NULL,
|
||||||
|
program_type VARCHAR(10) NOT NULL,
|
||||||
|
program_url VARCHAR(200),
|
||||||
|
category VARCHAR(50),
|
||||||
|
use_yn CHAR(1) NOT NULL DEFAULT 'Y',
|
||||||
|
created_by VARCHAR(20) NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
updated_by VARCHAR(20),
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
CONSTRAINT pk_tb_uiws_program PRIMARY KEY (program_id),
|
||||||
|
CONSTRAINT ck_uiws_program_type CHECK (program_type IN ('FORM','POPUP')),
|
||||||
|
CONSTRAINT ck_uiws_program_use_yn CHECK (use_yn IN ('Y','N'))
|
||||||
|
);
|
||||||
|
COMMENT ON TABLE tb_uiws_program IS 'UIWS 이식: 프로그램(화면 FORM/POPUP)';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tb_uiws_menu (
|
||||||
|
menu_id VARCHAR(20) NOT NULL,
|
||||||
|
menu_nm VARCHAR(100) NOT NULL,
|
||||||
|
parent_menu_id VARCHAR(20),
|
||||||
|
program_id VARCHAR(20),
|
||||||
|
menu_url VARCHAR(200),
|
||||||
|
sort_ord INT DEFAULT 0,
|
||||||
|
use_yn CHAR(1) NOT NULL DEFAULT 'Y',
|
||||||
|
created_by VARCHAR(20) NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
updated_by VARCHAR(20),
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
CONSTRAINT pk_tb_uiws_menu PRIMARY KEY (menu_id),
|
||||||
|
CONSTRAINT fk_uiws_menu_parent FOREIGN KEY (parent_menu_id) REFERENCES tb_uiws_menu (menu_id),
|
||||||
|
CONSTRAINT fk_uiws_menu_program FOREIGN KEY (program_id) REFERENCES tb_uiws_program (program_id),
|
||||||
|
CONSTRAINT ck_uiws_menu_use_yn CHECK (use_yn IN ('Y','N'))
|
||||||
|
);
|
||||||
|
COMMENT ON TABLE tb_uiws_menu IS 'UIWS 이식: 메뉴 (2-depth 자기참조, 프로그램 연결)';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tb_uiws_role_menu (
|
||||||
|
role_id VARCHAR(20) NOT NULL,
|
||||||
|
menu_id VARCHAR(20) NOT NULL,
|
||||||
|
read_yn CHAR(1) NOT NULL DEFAULT 'Y',
|
||||||
|
write_yn CHAR(1) NOT NULL DEFAULT 'N',
|
||||||
|
created_by VARCHAR(20) NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
updated_by VARCHAR(20),
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
CONSTRAINT pk_tb_uiws_role_menu PRIMARY KEY (role_id, menu_id),
|
||||||
|
CONSTRAINT fk_uiws_rolemenu_role FOREIGN KEY (role_id) REFERENCES tb_uiws_role (role_id),
|
||||||
|
CONSTRAINT fk_uiws_rolemenu_menu FOREIGN KEY (menu_id) REFERENCES tb_uiws_menu (menu_id),
|
||||||
|
CONSTRAINT ck_uiws_rolemenu_read_yn CHECK (read_yn IN ('Y','N')),
|
||||||
|
CONSTRAINT ck_uiws_rolemenu_write_yn CHECK (write_yn IN ('Y','N'))
|
||||||
|
);
|
||||||
|
COMMENT ON TABLE tb_uiws_role_menu IS 'UIWS 이식: 권한-메뉴 매핑 (복합 PK, 조회/등록 권한)';
|
||||||
|
|
||||||
|
-- 조회 보조 인덱스
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_uiws_sysuser_dept ON tb_uiws_sys_user (dept_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_uiws_sysuser_company ON tb_uiws_sys_user (company_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_uiws_menu_parent ON tb_uiws_menu (parent_menu_id, sort_ord);
|
||||||
|
|
||||||
|
-- ───────────────────────────────────────────────────────────────────────────
|
||||||
|
-- 최소 시드(멱등): 기본 권한 + 거래처 + 부서. ON CONFLICT DO NOTHING.
|
||||||
|
-- ───────────────────────────────────────────────────────────────────────────
|
||||||
|
INSERT INTO tb_uiws_role (role_id, role_nm, role_desc, use_yn, created_by)
|
||||||
|
VALUES ('ADMIN', '관리자', '시스템 관리자', 'Y', 'SYSTEM'),
|
||||||
|
('MANAGER', '매니저', '팀 관리자', 'Y', 'SYSTEM'),
|
||||||
|
('USER', '일반사용자', '일반 사용자', 'Y', 'SYSTEM')
|
||||||
|
ON CONFLICT (role_id) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO tb_uiws_company (company_id, company_nm, use_yn, created_by)
|
||||||
|
VALUES ('ZIOINFO', '지오정보기술', 'Y', 'SYSTEM')
|
||||||
|
ON CONFLICT (company_id) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO tb_uiws_dept (dept_id, dept_nm, parent_dept_id, sort_ord, use_yn, created_by)
|
||||||
|
VALUES ('ROOT', '본사', NULL, 0, 'Y', 'SYSTEM')
|
||||||
|
ON CONFLICT (dept_id) DO NOTHING;
|
||||||
|
|
||||||
|
-- ───────────────────────────────────────────────────────────────────────────
|
||||||
|
-- 로그인 보조(회원가입·아이디찾기·비번초기화) 이식: esn_user 보강 컬럼.
|
||||||
|
-- approved 기본값 true → 기존 계정/시드는 그대로 로그인 가능(회귀 0). 신규 회원가입만 approved=false 로 INSERT,
|
||||||
|
-- ADMIN 승인 전 로그인 차단(AuthService.login 의 approved 게이트에서 검사).
|
||||||
|
-- display_name: 아이디찾기(이름+이메일 매칭)용. pw_change_yn: 임시비번 발급 후 변경 유도 플래그.
|
||||||
|
-- ───────────────────────────────────────────────────────────────────────────
|
||||||
|
ALTER TABLE esn_user ADD COLUMN IF NOT EXISTS approved BOOLEAN DEFAULT true;
|
||||||
|
ALTER TABLE esn_user ADD COLUMN IF NOT EXISTS display_name VARCHAR(100);
|
||||||
|
ALTER TABLE esn_user ADD COLUMN IF NOT EXISTS pw_change_yn BOOLEAN DEFAULT false;
|
||||||
|
UPDATE esn_user SET approved = true WHERE approved IS NULL;
|
||||||
|
|
||||||
|
-- end 90_uiws_system.sql
|
||||||
12
backend/src/main/resources/db/93_esn_login_helper.sql
Normal file
12
backend/src/main/resources/db/93_esn_login_helper.sql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- 로그인 보조 3종(회원가입 승인대기 / 아이디찾기 / 비밀번호 초기화) — esn_user ALTER
|
||||||
|
-- 멱등: ADD COLUMN IF NOT EXISTS. mode:always 재실행 완전 멱등(기존 데이터 무영향).
|
||||||
|
-- - name : 아이디찾기/비번초기화 매칭 키(이름+이메일).
|
||||||
|
-- - approval_status : 가입 신청 상태. 기존 행 기본 APPROVED, 신규 가입 PENDING(is_active=FALSE).
|
||||||
|
-- 보안: 임시비밀번호/자격증명은 메일/로그 채널로만 전달(컬럼/응답 노출 없음, 불변규칙).
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
SET client_encoding = 'UTF8';
|
||||||
|
|
||||||
|
ALTER TABLE esn_user ADD COLUMN IF NOT EXISTS name VARCHAR(100);
|
||||||
|
ALTER TABLE esn_user ADD COLUMN IF NOT EXISTS approval_status VARCHAR(20) DEFAULT 'APPROVED';
|
||||||
@ -14,6 +14,9 @@
|
|||||||
<result property="active" column="is_active"/>
|
<result property="active" column="is_active"/>
|
||||||
<result property="lastLoginAt" column="last_login_at"/>
|
<result property="lastLoginAt" column="last_login_at"/>
|
||||||
<result property="createdAt" column="created_at"/>
|
<result property="createdAt" column="created_at"/>
|
||||||
|
<!-- 로그인 보조 이식 컬럼 (93_esn_login_helper.sql ALTER). -->
|
||||||
|
<result property="name" column="name"/>
|
||||||
|
<result property="approvalStatus" column="approval_status"/>
|
||||||
<!-- UIWS 2FA 이식 컬럼 (91_uiws_port.sql ALTER). -->
|
<!-- UIWS 2FA 이식 컬럼 (91_uiws_port.sql ALTER). -->
|
||||||
<result property="emailVerifyCode" column="email_verify_code"/>
|
<result property="emailVerifyCode" column="email_verify_code"/>
|
||||||
<result property="emailVerifyExpire" column="email_verify_expire"/>
|
<result property="emailVerifyExpire" column="email_verify_expire"/>
|
||||||
@ -24,7 +27,7 @@
|
|||||||
|
|
||||||
<select id="findByUsername" resultMap="userMap">
|
<select id="findByUsername" resultMap="userMap">
|
||||||
SELECT id, tenant_code, username, password_hash, role, email, phone,
|
SELECT id, tenant_code, username, password_hash, role, email, phone,
|
||||||
is_active, last_login_at, created_at,
|
is_active, last_login_at, created_at, name, approval_status,
|
||||||
email_verify_code, email_verify_expire, login_fail_count, locked, otp_secret
|
email_verify_code, email_verify_expire, login_fail_count, locked, otp_secret
|
||||||
FROM esn_user
|
FROM esn_user
|
||||||
WHERE username = #{username}
|
WHERE username = #{username}
|
||||||
@ -61,4 +64,35 @@
|
|||||||
UPDATE esn_user SET locked = false, login_fail_count = 0 WHERE username = #{username}
|
UPDATE esn_user SET locked = false, login_fail_count = 0 WHERE username = #{username}
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
|
<!-- ── 로그인 보조 이식: 회원가입(승인대기)/아이디찾기/비밀번호 초기화 ────────────── -->
|
||||||
|
|
||||||
|
<select id="countByUsername" resultType="int">
|
||||||
|
SELECT COUNT(*) FROM esn_user WHERE username = #{username}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insertPendingUser">
|
||||||
|
INSERT INTO esn_user (tenant_code, username, password_hash, role, name, email, phone,
|
||||||
|
is_active, approval_status, created_at)
|
||||||
|
VALUES (#{tenantCode}, #{username}, #{passwordHash}, 'USER', #{name}, #{email}, #{phone},
|
||||||
|
FALSE, 'PENDING', NOW())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<select id="findUsernameByNameEmail" resultType="string">
|
||||||
|
SELECT username FROM esn_user
|
||||||
|
WHERE name = #{name} AND email = #{email}
|
||||||
|
LIMIT 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="findByUsernameNameEmail" resultMap="userMap">
|
||||||
|
SELECT id, tenant_code, username, password_hash, role, email, phone,
|
||||||
|
is_active, last_login_at, created_at, name, approval_status,
|
||||||
|
email_verify_code, email_verify_expire, login_fail_count, locked, otp_secret
|
||||||
|
FROM esn_user
|
||||||
|
WHERE username = #{username} AND name = #{name} AND email = #{email}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<update id="updatePasswordHash">
|
||||||
|
UPDATE esn_user SET password_hash = #{passwordHash} WHERE username = #{username}
|
||||||
|
</update>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
63
backend/src/main/resources/mapper/uiws/system/CodeMapper.xml
Normal file
63
backend/src/main/resources/mapper/uiws/system/CodeMapper.xml
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<!-- UIWS system 이식: 공통코드 그룹/값(tb_uiws_code_grp, tb_uiws_code). -->
|
||||||
|
<mapper namespace="com.zioinfo.esn.uiws.system.mapper.CodeMapper">
|
||||||
|
|
||||||
|
<sql id="kw">
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
AND (grp_cd ILIKE '%' || #{keyword} || '%' OR grp_nm ILIKE '%' || #{keyword} || '%')
|
||||||
|
</if>
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="searchGroups" resultType="com.zioinfo.esn.uiws.system.model.SysCodeGrp">
|
||||||
|
SELECT * FROM tb_uiws_code_grp WHERE 1=1 <include refid="kw"/>
|
||||||
|
ORDER BY grp_cd ASC
|
||||||
|
LIMIT #{size} OFFSET #{offset}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="countGroups" resultType="long">
|
||||||
|
SELECT COUNT(*) FROM tb_uiws_code_grp WHERE 1=1 <include refid="kw"/>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="findGroupById" resultType="com.zioinfo.esn.uiws.system.model.SysCodeGrp">
|
||||||
|
SELECT * FROM tb_uiws_code_grp WHERE grp_cd = #{grpCd}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="groupExists" resultType="boolean">
|
||||||
|
SELECT EXISTS(SELECT 1 FROM tb_uiws_code_grp WHERE grp_cd = #{grpCd})
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insertGroup" parameterType="com.zioinfo.esn.uiws.system.model.SysCodeGrp">
|
||||||
|
INSERT INTO tb_uiws_code_grp (grp_cd, grp_nm, use_yn, created_by, created_at)
|
||||||
|
VALUES (#{grpCd}, #{grpNm}, #{useYn}, #{createdBy}, now())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="updateGroup" parameterType="com.zioinfo.esn.uiws.system.model.SysCodeGrp">
|
||||||
|
UPDATE tb_uiws_code_grp SET
|
||||||
|
grp_nm = #{grpNm}, use_yn = #{useYn}, updated_by = #{updatedBy}, updated_at = now()
|
||||||
|
WHERE grp_cd = #{grpCd}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="deleteGroup">
|
||||||
|
DELETE FROM tb_uiws_code_grp WHERE grp_cd = #{grpCd}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<select id="findValuesByGrp" resultType="com.zioinfo.esn.uiws.system.model.SysCode">
|
||||||
|
SELECT * FROM tb_uiws_code WHERE grp_cd = #{grpCd} ORDER BY sort_ord ASC, code_val ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="findActiveValuesByGrp" resultType="com.zioinfo.esn.uiws.system.model.SysCode">
|
||||||
|
SELECT * FROM tb_uiws_code WHERE grp_cd = #{grpCd} AND use_yn = 'Y'
|
||||||
|
ORDER BY sort_ord ASC, code_val ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insertValue" parameterType="com.zioinfo.esn.uiws.system.model.SysCode">
|
||||||
|
INSERT INTO tb_uiws_code (grp_cd, code_val, code_nm, sort_ord, use_yn, created_by, created_at)
|
||||||
|
VALUES (#{grpCd}, #{codeVal}, #{codeNm}, #{sortOrd}, #{useYn}, #{createdBy}, now())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<delete id="deleteValuesByGrp">
|
||||||
|
DELETE FROM tb_uiws_code WHERE grp_cd = #{grpCd}
|
||||||
|
</delete>
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<!-- UIWS system 이식: 거래처(tb_uiws_company). -->
|
||||||
|
<mapper namespace="com.zioinfo.esn.uiws.system.mapper.CompanyMapper">
|
||||||
|
|
||||||
|
<sql id="kw">
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
AND (company_id ILIKE '%' || #{keyword} || '%' OR company_nm ILIKE '%' || #{keyword} || '%')
|
||||||
|
</if>
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="search" resultType="com.zioinfo.esn.uiws.system.model.SysCompany">
|
||||||
|
SELECT * FROM tb_uiws_company WHERE 1=1 <include refid="kw"/>
|
||||||
|
ORDER BY company_nm ASC, company_id ASC
|
||||||
|
LIMIT #{size} OFFSET #{offset}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="countSearch" resultType="long">
|
||||||
|
SELECT COUNT(*) FROM tb_uiws_company WHERE 1=1 <include refid="kw"/>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="searchActive" resultType="com.zioinfo.esn.uiws.system.model.SysCompany">
|
||||||
|
SELECT * FROM tb_uiws_company WHERE use_yn = 'Y' <include refid="kw"/>
|
||||||
|
ORDER BY company_nm ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="findActiveOrdered" resultType="com.zioinfo.esn.uiws.system.model.SysCompany">
|
||||||
|
SELECT * FROM tb_uiws_company WHERE use_yn = 'Y' ORDER BY company_nm ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="findAll" resultType="com.zioinfo.esn.uiws.system.model.SysCompany">
|
||||||
|
SELECT * FROM tb_uiws_company ORDER BY company_nm ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="findById" resultType="com.zioinfo.esn.uiws.system.model.SysCompany">
|
||||||
|
SELECT * FROM tb_uiws_company WHERE company_id = #{companyId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="existsById" resultType="boolean">
|
||||||
|
SELECT EXISTS(SELECT 1 FROM tb_uiws_company WHERE company_id = #{companyId})
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" parameterType="com.zioinfo.esn.uiws.system.model.SysCompany">
|
||||||
|
INSERT INTO tb_uiws_company (company_id, company_nm, biz_no, use_yn, created_by, created_at)
|
||||||
|
VALUES (#{companyId}, #{companyNm}, #{bizNo}, #{useYn}, #{createdBy}, now())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="update" parameterType="com.zioinfo.esn.uiws.system.model.SysCompany">
|
||||||
|
UPDATE tb_uiws_company SET
|
||||||
|
company_nm = #{companyNm}, biz_no = #{bizNo}, use_yn = #{useYn},
|
||||||
|
updated_by = #{updatedBy}, updated_at = now()
|
||||||
|
WHERE company_id = #{companyId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="deleteById">
|
||||||
|
DELETE FROM tb_uiws_company WHERE company_id = #{companyId}
|
||||||
|
</delete>
|
||||||
|
</mapper>
|
||||||
63
backend/src/main/resources/mapper/uiws/system/DeptMapper.xml
Normal file
63
backend/src/main/resources/mapper/uiws/system/DeptMapper.xml
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<!-- UIWS system 이식: 부서(tb_uiws_dept). -->
|
||||||
|
<mapper namespace="com.zioinfo.esn.uiws.system.mapper.DeptMapper">
|
||||||
|
|
||||||
|
<sql id="kw">
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
AND (dept_id ILIKE '%' || #{keyword} || '%' OR dept_nm ILIKE '%' || #{keyword} || '%')
|
||||||
|
</if>
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="search" resultType="com.zioinfo.esn.uiws.system.model.SysDept">
|
||||||
|
SELECT * FROM tb_uiws_dept WHERE 1=1 <include refid="kw"/>
|
||||||
|
ORDER BY sort_ord ASC, dept_id ASC
|
||||||
|
LIMIT #{size} OFFSET #{offset}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="countSearch" resultType="long">
|
||||||
|
SELECT COUNT(*) FROM tb_uiws_dept WHERE 1=1 <include refid="kw"/>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="searchActive" resultType="com.zioinfo.esn.uiws.system.model.SysDept">
|
||||||
|
SELECT * FROM tb_uiws_dept WHERE use_yn = 'Y' <include refid="kw"/>
|
||||||
|
ORDER BY sort_ord ASC, dept_id ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="findActiveOrdered" resultType="com.zioinfo.esn.uiws.system.model.SysDept">
|
||||||
|
SELECT * FROM tb_uiws_dept WHERE use_yn = 'Y' ORDER BY sort_ord ASC, dept_id ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="findAll" resultType="com.zioinfo.esn.uiws.system.model.SysDept">
|
||||||
|
SELECT * FROM tb_uiws_dept ORDER BY sort_ord ASC, dept_id ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="findById" resultType="com.zioinfo.esn.uiws.system.model.SysDept">
|
||||||
|
SELECT * FROM tb_uiws_dept WHERE dept_id = #{deptId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="existsById" resultType="boolean">
|
||||||
|
SELECT EXISTS(SELECT 1 FROM tb_uiws_dept WHERE dept_id = #{deptId})
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="existsByParentDeptId" resultType="boolean">
|
||||||
|
SELECT EXISTS(SELECT 1 FROM tb_uiws_dept WHERE parent_dept_id = #{parentDeptId})
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" parameterType="com.zioinfo.esn.uiws.system.model.SysDept">
|
||||||
|
INSERT INTO tb_uiws_dept (dept_id, dept_nm, parent_dept_id, sort_ord, use_yn, created_by, created_at)
|
||||||
|
VALUES (#{deptId}, #{deptNm}, #{parentDeptId}, #{sortOrd}, #{useYn}, #{createdBy}, now())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="update" parameterType="com.zioinfo.esn.uiws.system.model.SysDept">
|
||||||
|
UPDATE tb_uiws_dept SET
|
||||||
|
dept_nm = #{deptNm}, parent_dept_id = #{parentDeptId}, sort_ord = #{sortOrd},
|
||||||
|
use_yn = #{useYn}, updated_by = #{updatedBy}, updated_at = now()
|
||||||
|
WHERE dept_id = #{deptId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="deleteById">
|
||||||
|
DELETE FROM tb_uiws_dept WHERE dept_id = #{deptId}
|
||||||
|
</delete>
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<!-- UIWS system 이식: 부서-권한 매핑(tb_uiws_dept_role). -->
|
||||||
|
<mapper namespace="com.zioinfo.esn.uiws.system.mapper.DeptRoleMapper">
|
||||||
|
|
||||||
|
<select id="findRoleIdsByDeptId" resultType="string">
|
||||||
|
SELECT role_id FROM tb_uiws_dept_role WHERE dept_id = #{deptId} ORDER BY role_id ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="exists" resultType="boolean">
|
||||||
|
SELECT EXISTS(SELECT 1 FROM tb_uiws_dept_role WHERE dept_id = #{deptId} AND role_id = #{roleId})
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="existsByRoleId" resultType="boolean">
|
||||||
|
SELECT EXISTS(SELECT 1 FROM tb_uiws_dept_role WHERE role_id = #{roleId})
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert">
|
||||||
|
INSERT INTO tb_uiws_dept_role (dept_id, role_id, created_by, created_at)
|
||||||
|
VALUES (#{deptId}, #{roleId}, #{actor}, now())
|
||||||
|
ON CONFLICT (dept_id, role_id) DO NOTHING
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<delete id="delete">
|
||||||
|
DELETE FROM tb_uiws_dept_role WHERE dept_id = #{deptId} AND role_id = #{roleId}
|
||||||
|
</delete>
|
||||||
|
</mapper>
|
||||||
63
backend/src/main/resources/mapper/uiws/system/MenuMapper.xml
Normal file
63
backend/src/main/resources/mapper/uiws/system/MenuMapper.xml
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<!-- UIWS system 이식: 메뉴(tb_uiws_menu). -->
|
||||||
|
<mapper namespace="com.zioinfo.esn.uiws.system.mapper.MenuMapper">
|
||||||
|
|
||||||
|
<select id="searchAll" resultType="com.zioinfo.esn.uiws.system.model.SysMenu">
|
||||||
|
SELECT * FROM tb_uiws_menu ORDER BY sort_ord ASC, menu_id ASC
|
||||||
|
LIMIT #{size} OFFSET #{offset}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="countAll" resultType="long">
|
||||||
|
SELECT COUNT(*) FROM tb_uiws_menu
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="search" resultType="com.zioinfo.esn.uiws.system.model.SysMenu">
|
||||||
|
SELECT * FROM tb_uiws_menu
|
||||||
|
WHERE use_yn = 'Y'
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
AND (menu_id ILIKE '%' || #{keyword} || '%' OR menu_nm ILIKE '%' || #{keyword} || '%')
|
||||||
|
</if>
|
||||||
|
ORDER BY sort_ord ASC, menu_id ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="findAllOrdered" resultType="com.zioinfo.esn.uiws.system.model.SysMenu">
|
||||||
|
SELECT * FROM tb_uiws_menu ORDER BY sort_ord ASC, menu_id ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="findById" resultType="com.zioinfo.esn.uiws.system.model.SysMenu">
|
||||||
|
SELECT * FROM tb_uiws_menu WHERE menu_id = #{menuId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="existsById" resultType="boolean">
|
||||||
|
SELECT EXISTS(SELECT 1 FROM tb_uiws_menu WHERE menu_id = #{menuId})
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="existsByParentMenuId" resultType="boolean">
|
||||||
|
SELECT EXISTS(SELECT 1 FROM tb_uiws_menu WHERE parent_menu_id = #{parentMenuId})
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="existsByProgramId" resultType="boolean">
|
||||||
|
SELECT EXISTS(SELECT 1 FROM tb_uiws_menu WHERE program_id = #{programId})
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" parameterType="com.zioinfo.esn.uiws.system.model.SysMenu">
|
||||||
|
INSERT INTO tb_uiws_menu
|
||||||
|
(menu_id, menu_nm, parent_menu_id, program_id, menu_url, sort_ord, use_yn, created_by, created_at)
|
||||||
|
VALUES
|
||||||
|
(#{menuId}, #{menuNm}, #{parentMenuId}, #{programId}, #{menuUrl}, #{sortOrd}, #{useYn}, #{createdBy}, now())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="update" parameterType="com.zioinfo.esn.uiws.system.model.SysMenu">
|
||||||
|
UPDATE tb_uiws_menu SET
|
||||||
|
menu_nm = #{menuNm}, parent_menu_id = #{parentMenuId}, program_id = #{programId},
|
||||||
|
menu_url = #{menuUrl}, sort_ord = #{sortOrd}, use_yn = #{useYn},
|
||||||
|
updated_by = #{updatedBy}, updated_at = now()
|
||||||
|
WHERE menu_id = #{menuId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="deleteById">
|
||||||
|
DELETE FROM tb_uiws_menu WHERE menu_id = #{menuId}
|
||||||
|
</delete>
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<!-- UIWS system 이식: 프로그램(tb_uiws_program). -->
|
||||||
|
<mapper namespace="com.zioinfo.esn.uiws.system.mapper.ProgramMapper">
|
||||||
|
|
||||||
|
<sql id="searchWhere">
|
||||||
|
WHERE 1=1
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
AND (program_id ILIKE '%' || #{keyword} || '%' OR program_nm ILIKE '%' || #{keyword} || '%')
|
||||||
|
</if>
|
||||||
|
<if test="programType != null and programType != ''">AND program_type = #{programType}</if>
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="search" resultType="com.zioinfo.esn.uiws.system.model.SysProgram">
|
||||||
|
SELECT * FROM tb_uiws_program <include refid="searchWhere"/>
|
||||||
|
ORDER BY program_id ASC
|
||||||
|
LIMIT #{size} OFFSET #{offset}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="countSearch" resultType="long">
|
||||||
|
SELECT COUNT(*) FROM tb_uiws_program <include refid="searchWhere"/>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="searchActive" resultType="com.zioinfo.esn.uiws.system.model.SysProgram">
|
||||||
|
SELECT * FROM tb_uiws_program
|
||||||
|
WHERE use_yn = 'Y'
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
AND (program_id ILIKE '%' || #{keyword} || '%' OR program_nm ILIKE '%' || #{keyword} || '%')
|
||||||
|
</if>
|
||||||
|
ORDER BY program_id ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="findById" resultType="com.zioinfo.esn.uiws.system.model.SysProgram">
|
||||||
|
SELECT * FROM tb_uiws_program WHERE program_id = #{programId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="existsById" resultType="boolean">
|
||||||
|
SELECT EXISTS(SELECT 1 FROM tb_uiws_program WHERE program_id = #{programId})
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" parameterType="com.zioinfo.esn.uiws.system.model.SysProgram">
|
||||||
|
INSERT INTO tb_uiws_program
|
||||||
|
(program_id, program_nm, program_type, program_url, category, use_yn, created_by, created_at)
|
||||||
|
VALUES
|
||||||
|
(#{programId}, #{programNm}, #{programType}, #{programUrl}, #{category}, #{useYn}, #{createdBy}, now())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="update" parameterType="com.zioinfo.esn.uiws.system.model.SysProgram">
|
||||||
|
UPDATE tb_uiws_program SET
|
||||||
|
program_nm = #{programNm}, program_type = #{programType}, program_url = #{programUrl},
|
||||||
|
category = #{category}, use_yn = #{useYn}, updated_by = #{updatedBy}, updated_at = now()
|
||||||
|
WHERE program_id = #{programId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="deleteById">
|
||||||
|
DELETE FROM tb_uiws_program WHERE program_id = #{programId}
|
||||||
|
</delete>
|
||||||
|
</mapper>
|
||||||
46
backend/src/main/resources/mapper/uiws/system/RoleMapper.xml
Normal file
46
backend/src/main/resources/mapper/uiws/system/RoleMapper.xml
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<!-- UIWS system 이식: 권한(tb_uiws_role). -->
|
||||||
|
<mapper namespace="com.zioinfo.esn.uiws.system.mapper.RoleMapper">
|
||||||
|
|
||||||
|
<sql id="kw">
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
AND (role_id ILIKE '%' || #{keyword} || '%' OR role_nm ILIKE '%' || #{keyword} || '%')
|
||||||
|
</if>
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="search" resultType="com.zioinfo.esn.uiws.system.model.SysRole">
|
||||||
|
SELECT * FROM tb_uiws_role WHERE 1=1 <include refid="kw"/>
|
||||||
|
ORDER BY role_id ASC
|
||||||
|
LIMIT #{size} OFFSET #{offset}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="countSearch" resultType="long">
|
||||||
|
SELECT COUNT(*) FROM tb_uiws_role WHERE 1=1 <include refid="kw"/>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="findById" resultType="com.zioinfo.esn.uiws.system.model.SysRole">
|
||||||
|
SELECT * FROM tb_uiws_role WHERE role_id = #{roleId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="existsById" resultType="boolean">
|
||||||
|
SELECT EXISTS(SELECT 1 FROM tb_uiws_role WHERE role_id = #{roleId})
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" parameterType="com.zioinfo.esn.uiws.system.model.SysRole">
|
||||||
|
INSERT INTO tb_uiws_role (role_id, role_nm, role_desc, use_yn, created_by, created_at)
|
||||||
|
VALUES (#{roleId}, #{roleNm}, #{roleDesc}, #{useYn}, #{createdBy}, now())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="update" parameterType="com.zioinfo.esn.uiws.system.model.SysRole">
|
||||||
|
UPDATE tb_uiws_role SET
|
||||||
|
role_nm = #{roleNm}, role_desc = #{roleDesc}, use_yn = #{useYn},
|
||||||
|
updated_by = #{updatedBy}, updated_at = now()
|
||||||
|
WHERE role_id = #{roleId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="deleteById">
|
||||||
|
DELETE FROM tb_uiws_role WHERE role_id = #{roleId}
|
||||||
|
</delete>
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<!-- UIWS system 이식: 권한-메뉴 매핑(tb_uiws_role_menu). -->
|
||||||
|
<mapper namespace="com.zioinfo.esn.uiws.system.mapper.RoleMenuMapper">
|
||||||
|
|
||||||
|
<select id="findByRoleId" resultType="com.zioinfo.esn.uiws.system.model.SysRoleMenu">
|
||||||
|
SELECT * FROM tb_uiws_role_menu WHERE role_id = #{roleId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="existsByMenuId" resultType="boolean">
|
||||||
|
SELECT EXISTS(SELECT 1 FROM tb_uiws_role_menu WHERE menu_id = #{menuId})
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" parameterType="com.zioinfo.esn.uiws.system.model.SysRoleMenu">
|
||||||
|
INSERT INTO tb_uiws_role_menu (role_id, menu_id, read_yn, write_yn, created_by, created_at)
|
||||||
|
VALUES (#{roleId}, #{menuId}, #{readYn}, #{writeYn}, #{createdBy}, now())
|
||||||
|
ON CONFLICT (role_id, menu_id) DO UPDATE SET
|
||||||
|
read_yn = EXCLUDED.read_yn, write_yn = EXCLUDED.write_yn
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<delete id="deleteByRoleId">
|
||||||
|
DELETE FROM tb_uiws_role_menu WHERE role_id = #{roleId}
|
||||||
|
</delete>
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<!-- UIWS system 이식: 업무 사용자(tb_uiws_sys_user). password 는 BCrypt, 응답 DTO 에는 미포함. -->
|
||||||
|
<mapper namespace="com.zioinfo.esn.uiws.system.mapper.SysUserMapper">
|
||||||
|
|
||||||
|
<sql id="searchWhere">
|
||||||
|
WHERE 1=1
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
AND (user_id ILIKE '%' || #{keyword} || '%'
|
||||||
|
OR user_nm ILIKE '%' || #{keyword} || '%'
|
||||||
|
OR email ILIKE '%' || #{keyword} || '%')
|
||||||
|
</if>
|
||||||
|
<if test="deptId != null and deptId != ''">AND dept_id = #{deptId}</if>
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="search" resultType="com.zioinfo.esn.uiws.system.model.SysUser">
|
||||||
|
SELECT * FROM tb_uiws_sys_user <include refid="searchWhere"/>
|
||||||
|
ORDER BY user_nm ASC, user_id ASC
|
||||||
|
LIMIT #{size} OFFSET #{offset}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="countSearch" resultType="long">
|
||||||
|
SELECT COUNT(*) FROM tb_uiws_sys_user <include refid="searchWhere"/>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="searchActive" resultType="com.zioinfo.esn.uiws.system.model.SysUser">
|
||||||
|
SELECT * FROM tb_uiws_sys_user
|
||||||
|
WHERE use_yn = 'Y'
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
AND (user_id ILIKE '%' || #{keyword} || '%' OR user_nm ILIKE '%' || #{keyword} || '%')
|
||||||
|
</if>
|
||||||
|
<if test="deptId != null and deptId != ''">AND dept_id = #{deptId}</if>
|
||||||
|
ORDER BY user_nm ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="findByDeptId" resultType="com.zioinfo.esn.uiws.system.model.SysUser">
|
||||||
|
SELECT * FROM tb_uiws_sys_user WHERE dept_id = #{deptId} ORDER BY user_nm ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="findById" resultType="com.zioinfo.esn.uiws.system.model.SysUser">
|
||||||
|
SELECT * FROM tb_uiws_sys_user WHERE user_id = #{userId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="existsById" resultType="boolean">
|
||||||
|
SELECT EXISTS(SELECT 1 FROM tb_uiws_sys_user WHERE user_id = #{userId})
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" parameterType="com.zioinfo.esn.uiws.system.model.SysUser">
|
||||||
|
INSERT INTO tb_uiws_sys_user
|
||||||
|
(user_id, user_nm, password, email, grade_cd, dept_id, company_id, role_cd,
|
||||||
|
naverworks_id, login_fail_cnt, lock_yn, use_yn, approval_yn, verify_method,
|
||||||
|
pw_change_yn, created_by, created_at)
|
||||||
|
VALUES
|
||||||
|
(#{userId}, #{userNm}, #{password}, #{email}, #{gradeCd}, #{deptId}, #{companyId}, #{roleCd},
|
||||||
|
#{naverworksId}, #{loginFailCnt}, #{lockYn}, #{useYn}, #{approvalYn}, #{verifyMethod},
|
||||||
|
#{pwChangeYn}, #{createdBy}, now())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="update" parameterType="com.zioinfo.esn.uiws.system.model.SysUser">
|
||||||
|
UPDATE tb_uiws_sys_user SET
|
||||||
|
user_nm = #{userNm}, email = #{email}, grade_cd = #{gradeCd},
|
||||||
|
dept_id = #{deptId}, company_id = #{companyId}, role_cd = #{roleCd},
|
||||||
|
naverworks_id = #{naverworksId}, lock_yn = #{lockYn}, use_yn = #{useYn},
|
||||||
|
approval_yn = #{approvalYn},
|
||||||
|
<if test="password != null">password = #{password},</if>
|
||||||
|
updated_by = #{updatedBy}, updated_at = now()
|
||||||
|
WHERE user_id = #{userId}
|
||||||
|
</update>
|
||||||
|
</mapper>
|
||||||
@ -29,6 +29,21 @@ export const login = (username: string, password: string) =>
|
|||||||
export const getMe = () => api.get('/api/auth/me')
|
export const getMe = () => api.get('/api/auth/me')
|
||||||
export const logout = () => api.post('/api/auth/logout')
|
export const logout = () => api.post('/api/auth/logout')
|
||||||
|
|
||||||
|
// ── Auth: 로그인 보조 3종 (공개 — 회원가입 승인대기 / 아이디찾기 / 비밀번호 초기화) ──
|
||||||
|
export interface SignupReq {
|
||||||
|
username: string; password: string; name: string;
|
||||||
|
email: string; phone: string; tenantCode: string
|
||||||
|
}
|
||||||
|
export interface SignupRes { username: string; status: string }
|
||||||
|
export interface FindIdRes { found: boolean; maskedUsername: string }
|
||||||
|
|
||||||
|
export const signup = (d: SignupReq) =>
|
||||||
|
unwrap(api.post('/api/auth/signup', d)) as Promise<SignupRes>
|
||||||
|
export const findId = (name: string, email: string) =>
|
||||||
|
unwrap(api.post('/api/auth/find-id', { name, email })) as Promise<FindIdRes>
|
||||||
|
export const resetPassword = (username: string, name: string, email: string) =>
|
||||||
|
api.post('/api/auth/reset-password', { username, name, email })
|
||||||
|
|
||||||
// ── Dashboard ────────────────────────────────────────────────────────────
|
// ── Dashboard ────────────────────────────────────────────────────────────
|
||||||
export const getDashboard = (tenantCode?: string) =>
|
export const getDashboard = (tenantCode?: string) =>
|
||||||
unwrap(api.get(`/api/dashboard${tenantCode ? `?tenantCode=${tenantCode}` : ''}`))
|
unwrap(api.get(`/api/dashboard${tenantCode ? `?tenantCode=${tenantCode}` : ''}`))
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { login } from '../api/client'
|
import { login, signup, findId, resetPassword } from '../api/client'
|
||||||
import { verify2fa } from '../api/uiws'
|
import { verify2fa } from '../api/uiws'
|
||||||
|
|
||||||
|
type HelperMode = null | 'signup' | 'findId' | 'resetPw'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 로그인 화면. UIWS 2FA 이식 반영:
|
* 로그인 화면. UIWS 2FA 이식 반영:
|
||||||
* - 2FA off 응답({ twofa:"false", token }) → 기존처럼 즉시 로그인(회귀 0).
|
* - 2FA off 응답({ twofa:"false", token }) → 기존처럼 즉시 로그인(회귀 0).
|
||||||
@ -20,6 +22,9 @@ export default function Login() {
|
|||||||
const [maskedEmail, setMaskedEmail] = useState('')
|
const [maskedEmail, setMaskedEmail] = useState('')
|
||||||
const [code, setCode] = useState('')
|
const [code, setCode] = useState('')
|
||||||
|
|
||||||
|
// ── 로그인 보조 3종 (회원가입/아이디찾기/비밀번호 재설정) 모달 ──────────────────
|
||||||
|
const [helper, setHelper] = useState<HelperMode>(null)
|
||||||
|
|
||||||
function finishLogin(token: string) {
|
function finishLogin(token: string) {
|
||||||
localStorage.setItem('esn_token', token)
|
localStorage.setItem('esn_token', token)
|
||||||
localStorage.setItem('esn_user', username)
|
localStorage.setItem('esn_user', username)
|
||||||
@ -101,6 +106,16 @@ export default function Login() {
|
|||||||
>
|
>
|
||||||
{loading ? '로그인 중...' : '로그인'}
|
{loading ? '로그인 중...' : '로그인'}
|
||||||
</button>
|
</button>
|
||||||
|
<div className="flex items-center justify-center gap-3 pt-1 text-xs text-gray-400">
|
||||||
|
<button type="button" onClick={() => setHelper('signup')}
|
||||||
|
className="hover:text-brand transition-colors">회원가입</button>
|
||||||
|
<span className="text-edge">|</span>
|
||||||
|
<button type="button" onClick={() => setHelper('findId')}
|
||||||
|
className="hover:text-brand transition-colors">아이디 찾기</button>
|
||||||
|
<span className="text-edge">|</span>
|
||||||
|
<button type="button" onClick={() => setHelper('resetPw')}
|
||||||
|
className="hover:text-brand transition-colors">비밀번호 재설정</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
<form onSubmit={handleVerify} className="bg-card border border-edge rounded-lg p-6 space-y-4">
|
<form onSubmit={handleVerify} className="bg-card border border-edge rounded-lg p-6 space-y-4">
|
||||||
@ -136,6 +151,112 @@ export default function Login() {
|
|||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{helper && <HelperModal mode={helper} onClose={() => setHelper(null)} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 로그인 보조 모달 — 회원가입 / 아이디찾기 / 비밀번호 재설정. esn 토큰·테마 토큰만 사용. */
|
||||||
|
function HelperModal({ mode, onClose }: { mode: Exclude<HelperMode, null>; onClose: () => void }) {
|
||||||
|
const [busy, setBusy] = useState(false)
|
||||||
|
const [msg, setMsg] = useState('')
|
||||||
|
const [err, setErr] = useState('')
|
||||||
|
|
||||||
|
// signup
|
||||||
|
const [su, setSu] = useState({
|
||||||
|
username: '', password: '', name: '', email: '', phone: '', tenantCode: '',
|
||||||
|
})
|
||||||
|
// findId
|
||||||
|
const [fi, setFi] = useState({ name: '', email: '' })
|
||||||
|
// resetPw
|
||||||
|
const [rp, setRp] = useState({ username: '', name: '', email: '' })
|
||||||
|
|
||||||
|
const title =
|
||||||
|
mode === 'signup' ? '회원가입' : mode === 'findId' ? '아이디 찾기' : '비밀번호 재설정'
|
||||||
|
|
||||||
|
async function onSubmit(e: React.FormEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
setBusy(true); setMsg(''); setErr('')
|
||||||
|
try {
|
||||||
|
if (mode === 'signup') {
|
||||||
|
const res = await signup(su)
|
||||||
|
setMsg(`가입 신청이 접수되었습니다 (아이디: ${res.username}, 상태: 승인 대기).`)
|
||||||
|
} else if (mode === 'findId') {
|
||||||
|
const res = await findId(fi.name, fi.email)
|
||||||
|
setMsg(res.found ? `회원님의 아이디: ${res.maskedUsername}` : '일치하는 계정 없음')
|
||||||
|
} else {
|
||||||
|
await resetPassword(rp.username, rp.name, rp.email)
|
||||||
|
setMsg('임시 비밀번호를 이메일로 발송했습니다.')
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
setErr(e?.response?.data?.message || '요청 처리 중 오류가 발생했습니다.')
|
||||||
|
} finally {
|
||||||
|
setBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const field =
|
||||||
|
'w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white placeholder-gray-600 focus:border-brand focus:outline-none'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4"
|
||||||
|
onClick={onClose}>
|
||||||
|
<div className="w-full max-w-sm bg-card border border-edge rounded-lg p-6"
|
||||||
|
onClick={e => e.stopPropagation()}>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-sm font-bold text-brand">{title}</h2>
|
||||||
|
<button type="button" onClick={onClose}
|
||||||
|
className="text-gray-500 hover:text-white text-sm">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={onSubmit} className="space-y-3">
|
||||||
|
{mode === 'signup' && (
|
||||||
|
<>
|
||||||
|
<input className={field} placeholder="아이디" value={su.username}
|
||||||
|
onChange={e => setSu({ ...su, username: e.target.value })} />
|
||||||
|
<input className={field} type="password" placeholder="비밀번호" value={su.password}
|
||||||
|
onChange={e => setSu({ ...su, password: e.target.value })} />
|
||||||
|
<input className={field} placeholder="이름" value={su.name}
|
||||||
|
onChange={e => setSu({ ...su, name: e.target.value })} />
|
||||||
|
<input className={field} type="email" placeholder="이메일" value={su.email}
|
||||||
|
onChange={e => setSu({ ...su, email: e.target.value })} />
|
||||||
|
<input className={field} placeholder="연락처" value={su.phone}
|
||||||
|
onChange={e => setSu({ ...su, phone: e.target.value })} />
|
||||||
|
<input className={field} placeholder="기관 코드 (선택)" value={su.tenantCode}
|
||||||
|
onChange={e => setSu({ ...su, tenantCode: e.target.value })} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{mode === 'findId' && (
|
||||||
|
<>
|
||||||
|
<input className={field} placeholder="이름" value={fi.name}
|
||||||
|
onChange={e => setFi({ ...fi, name: e.target.value })} />
|
||||||
|
<input className={field} type="email" placeholder="이메일" value={fi.email}
|
||||||
|
onChange={e => setFi({ ...fi, email: e.target.value })} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{mode === 'resetPw' && (
|
||||||
|
<>
|
||||||
|
<input className={field} placeholder="아이디" value={rp.username}
|
||||||
|
onChange={e => setRp({ ...rp, username: e.target.value })} />
|
||||||
|
<input className={field} placeholder="이름" value={rp.name}
|
||||||
|
onChange={e => setRp({ ...rp, name: e.target.value })} />
|
||||||
|
<input className={field} type="email" placeholder="이메일" value={rp.email}
|
||||||
|
onChange={e => setRp({ ...rp, email: e.target.value })} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{msg && <p className="text-green-400 text-xs">{msg}</p>}
|
||||||
|
{err && <p className="text-red-400 text-xs">{err}</p>}
|
||||||
|
|
||||||
|
<button type="submit" disabled={busy}
|
||||||
|
className="w-full bg-brand hover:bg-brand2 text-white py-2 rounded text-sm font-medium transition-colors disabled:opacity-50">
|
||||||
|
{busy ? '처리 중...' : title}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user