From c327889a186b9be91d7cd12644ecf2b0d905e8e8 Mon Sep 17 00:00:00 2001 From: GUARDiA Date: Sun, 21 Jun 2026 20:59:13 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20UIMS=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=EB=B3=B4=EC=A1=B0+=EA=B6=8C=ED=95=9C=EA=B4=80=EB=A6=AC(RBAC)?= =?UTF-8?q?=20=EC=9D=B4=EC=8B=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cms/auth/AuthHelperController.java | 51 ++++ .../com/zioinfo/cms/auth/AuthService.java | 115 +++++++++ .../java/com/zioinfo/cms/auth/CmsUser.java | 4 + .../cms/auth/dto/AuthHelperResult.java | 11 + .../zioinfo/cms/auth/dto/FindIdRequest.java | 11 + .../zioinfo/cms/auth/dto/FindIdResponse.java | 11 + .../cms/auth/dto/ResetPasswordRequest.java | 11 + .../zioinfo/cms/auth/dto/SignupRequest.java | 13 + .../zioinfo/cms/auth/mapper/UserMapper.java | 26 ++ .../zioinfo/cms/config/SecurityConfig.java | 9 + .../cms/uiws/common/UiwsErrorCode.java | 14 +- .../system/controller/CodeController.java | 58 +++++ .../system/controller/CompanyController.java | 56 +++++ .../system/controller/DeptController.java | 61 +++++ .../system/controller/DeptRoleController.java | 39 +++ .../system/controller/MenuController.java | 55 ++++ .../system/controller/ProgramController.java | 57 +++++ .../controller/PublicLookupController.java | 29 +++ .../system/controller/RoleController.java | 49 ++++ .../system/controller/RoleMenuController.java | 39 +++ .../system/controller/UserController.java | 89 +++++++ .../cms/uiws/system/dto/CheckIdResponse.java | 4 + .../cms/uiws/system/dto/CodeGrpDetailDto.java | 6 + .../cms/uiws/system/dto/CodeGrpDto.java | 4 + .../cms/uiws/system/dto/CodeGrpSaveDto.java | 11 + .../cms/uiws/system/dto/CodeValueDto.java | 4 + .../cms/uiws/system/dto/CompanyDto.java | 4 + .../cms/uiws/system/dto/CompanySaveDto.java | 10 + .../zioinfo/cms/uiws/system/dto/DeptDto.java | 4 + .../cms/uiws/system/dto/DeptSaveDto.java | 11 + .../cms/uiws/system/dto/DeptTreeDto.java | 8 + .../cms/uiws/system/dto/DeptUserRoleDto.java | 6 + .../cms/uiws/system/dto/IdsRequest.java | 7 + .../zioinfo/cms/uiws/system/dto/MenuDto.java | 6 + .../cms/uiws/system/dto/MenuSaveDto.java | 10 + .../cms/uiws/system/dto/PageResponse.java | 18 ++ .../cms/uiws/system/dto/ProgramDto.java | 6 + .../cms/uiws/system/dto/ProgramSaveDto.java | 11 + .../cms/uiws/system/dto/PublicCompanyDto.java | 4 + .../cms/uiws/system/dto/PublicDeptDto.java | 4 + .../zioinfo/cms/uiws/system/dto/RoleDto.java | 4 + .../cms/uiws/system/dto/RoleIdsRequest.java | 7 + .../cms/uiws/system/dto/RoleMenuDto.java | 4 + .../uiws/system/dto/RoleMenuSaveRequest.java | 6 + .../cms/uiws/system/dto/RoleSaveDto.java | 10 + .../zioinfo/cms/uiws/system/dto/UserDto.java | 7 + .../cms/uiws/system/dto/UserSaveDto.java | 13 + .../cms/uiws/system/mapper/CodeMapper.java | 23 ++ .../cms/uiws/system/mapper/CompanyMapper.java | 22 ++ .../cms/uiws/system/mapper/DeptMapper.java | 23 ++ .../uiws/system/mapper/DeptRoleMapper.java | 16 ++ .../cms/uiws/system/mapper/MenuMapper.java | 22 ++ .../cms/uiws/system/mapper/ProgramMapper.java | 20 ++ .../cms/uiws/system/mapper/RoleMapper.java | 19 ++ .../uiws/system/mapper/RoleMenuMapper.java | 15 ++ .../cms/uiws/system/mapper/SysUserMapper.java | 20 ++ .../cms/uiws/system/model/SysCode.java | 18 ++ .../cms/uiws/system/model/SysCodeGrp.java | 16 ++ .../cms/uiws/system/model/SysCompany.java | 17 ++ .../cms/uiws/system/model/SysDept.java | 18 ++ .../cms/uiws/system/model/SysMenu.java | 20 ++ .../cms/uiws/system/model/SysProgram.java | 19 ++ .../cms/uiws/system/model/SysRole.java | 17 ++ .../cms/uiws/system/model/SysRoleMenu.java | 17 ++ .../cms/uiws/system/model/SysUser.java | 29 +++ .../cms/uiws/system/service/CodeService.java | 119 +++++++++ .../uiws/system/service/CompanyService.java | 89 +++++++ .../uiws/system/service/DeptRoleService.java | 61 +++++ .../cms/uiws/system/service/DeptService.java | 181 ++++++++++++++ .../cms/uiws/system/service/MenuService.java | 103 ++++++++ .../uiws/system/service/ProgramService.java | 98 ++++++++ .../system/service/PublicLookupService.java | 35 +++ .../uiws/system/service/RoleMenuService.java | 91 +++++++ .../cms/uiws/system/service/RoleService.java | 89 +++++++ .../cms/uiws/system/service/SysActor.java | 24 ++ .../cms/uiws/system/service/UserService.java | 234 ++++++++++++++++++ backend/src/main/resources/application.yml | 3 +- .../src/main/resources/db/91_uiws_port.sql | 5 + .../src/main/resources/db/92_uiws_system.sql | 229 +++++++++++++++++ .../src/main/resources/mapper/UserMapper.xml | 46 +++- .../mapper/uiws/system/CodeMapper.xml | 63 +++++ .../mapper/uiws/system/CompanyMapper.xml | 59 +++++ .../mapper/uiws/system/DeptMapper.xml | 63 +++++ .../mapper/uiws/system/DeptRoleMapper.xml | 28 +++ .../mapper/uiws/system/MenuMapper.xml | 63 +++++ .../mapper/uiws/system/ProgramMapper.xml | 59 +++++ .../mapper/uiws/system/RoleMapper.xml | 46 ++++ .../mapper/uiws/system/RoleMenuMapper.xml | 25 ++ .../mapper/uiws/system/SysUserMapper.xml | 70 ++++++ frontend/src/api/uiws.ts | 9 + frontend/src/pages/AuthHelperModals.tsx | 191 ++++++++++++++ frontend/src/pages/Login.tsx | 11 + 92 files changed, 3439 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/java/com/zioinfo/cms/auth/AuthHelperController.java create mode 100644 backend/src/main/java/com/zioinfo/cms/auth/dto/AuthHelperResult.java create mode 100644 backend/src/main/java/com/zioinfo/cms/auth/dto/FindIdRequest.java create mode 100644 backend/src/main/java/com/zioinfo/cms/auth/dto/FindIdResponse.java create mode 100644 backend/src/main/java/com/zioinfo/cms/auth/dto/ResetPasswordRequest.java create mode 100644 backend/src/main/java/com/zioinfo/cms/auth/dto/SignupRequest.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/controller/CodeController.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/controller/CompanyController.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/controller/DeptController.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/controller/DeptRoleController.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/controller/MenuController.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/controller/ProgramController.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/controller/PublicLookupController.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/controller/RoleController.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/controller/RoleMenuController.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/controller/UserController.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CheckIdResponse.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CodeGrpDetailDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CodeGrpDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CodeGrpSaveDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CodeValueDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CompanyDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CompanySaveDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/DeptDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/DeptSaveDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/DeptTreeDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/DeptUserRoleDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/IdsRequest.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/MenuDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/MenuSaveDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/PageResponse.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/ProgramDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/ProgramSaveDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/PublicCompanyDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/PublicDeptDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleIdsRequest.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleMenuDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleMenuSaveRequest.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleSaveDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/UserDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/dto/UserSaveDto.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/CodeMapper.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/CompanyMapper.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/DeptMapper.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/DeptRoleMapper.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/MenuMapper.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/ProgramMapper.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/RoleMapper.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/RoleMenuMapper.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/SysUserMapper.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysCode.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysCodeGrp.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysCompany.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysDept.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysMenu.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysProgram.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysRole.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysRoleMenu.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysUser.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/service/CodeService.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/service/CompanyService.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/service/DeptRoleService.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/service/DeptService.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/service/MenuService.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/service/ProgramService.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/service/PublicLookupService.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/service/RoleMenuService.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/service/RoleService.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/service/SysActor.java create mode 100644 backend/src/main/java/com/zioinfo/cms/uiws/system/service/UserService.java create mode 100644 backend/src/main/resources/db/92_uiws_system.sql create mode 100644 backend/src/main/resources/mapper/uiws/system/CodeMapper.xml create mode 100644 backend/src/main/resources/mapper/uiws/system/CompanyMapper.xml create mode 100644 backend/src/main/resources/mapper/uiws/system/DeptMapper.xml create mode 100644 backend/src/main/resources/mapper/uiws/system/DeptRoleMapper.xml create mode 100644 backend/src/main/resources/mapper/uiws/system/MenuMapper.xml create mode 100644 backend/src/main/resources/mapper/uiws/system/ProgramMapper.xml create mode 100644 backend/src/main/resources/mapper/uiws/system/RoleMapper.xml create mode 100644 backend/src/main/resources/mapper/uiws/system/RoleMenuMapper.xml create mode 100644 backend/src/main/resources/mapper/uiws/system/SysUserMapper.xml create mode 100644 frontend/src/pages/AuthHelperModals.tsx diff --git a/backend/src/main/java/com/zioinfo/cms/auth/AuthHelperController.java b/backend/src/main/java/com/zioinfo/cms/auth/AuthHelperController.java new file mode 100644 index 0000000..3343ec1 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/auth/AuthHelperController.java @@ -0,0 +1,51 @@ +package com.zioinfo.cms.auth; + +import com.zioinfo.cms.auth.dto.AuthHelperResult; +import com.zioinfo.cms.auth.dto.FindIdRequest; +import com.zioinfo.cms.auth.dto.FindIdResponse; +import com.zioinfo.cms.auth.dto.ResetPasswordRequest; +import com.zioinfo.cms.auth.dto.SignupRequest; +import com.zioinfo.cms.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종(UIWS auth 패턴 이식) — 회원가입 / 아이디찾기 / 비밀번호 초기화. + * + *

base path {@code /api/auth} (SecurityConfig permitAll — 로그인 전 무인증 접근). + * 대상은 CMS 관리자/운영자 계정(cms_user) — UGC 고객회원(cms_member)과 구분된다. + *

+ * 보안 불변규칙: 임시비번·자격증명·계정 존재여부를 응답/로그 메시지에 노출하지 않는다. + */ +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +public class AuthHelperController { + + private final AuthService authService; + + /** 운영자 회원가입(승인 대기 INSERT). */ + @PostMapping("/signup") + public ApiResponse signup(@RequestBody SignupRequest req) { + return ApiResponse.ok(authService.signup(req)); + } + + /** 아이디 찾기(이메일+이름 매칭, 마스킹 반환). */ + @PostMapping("/find-id") + public ApiResponse findId(@RequestBody FindIdRequest req) { + return ApiResponse.ok(authService.findId(req)); + } + + /** 비밀번호 초기화(검증 → 임시비번 BCrypt + 메일/로그). */ + @PostMapping("/reset-password") + public ApiResponse resetPassword(@RequestBody ResetPasswordRequest req) { + return ApiResponse.ok(authService.resetPassword(req)); + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/auth/AuthService.java b/backend/src/main/java/com/zioinfo/cms/auth/AuthService.java index bd73771..93d53ce 100644 --- a/backend/src/main/java/com/zioinfo/cms/auth/AuthService.java +++ b/backend/src/main/java/com/zioinfo/cms/auth/AuthService.java @@ -1,13 +1,22 @@ package com.zioinfo.cms.auth; +import com.zioinfo.cms.auth.dto.AuthHelperResult; +import com.zioinfo.cms.auth.dto.FindIdRequest; +import com.zioinfo.cms.auth.dto.FindIdResponse; +import com.zioinfo.cms.auth.dto.ResetPasswordRequest; +import com.zioinfo.cms.auth.dto.SignupRequest; import com.zioinfo.cms.auth.mapper.UserMapper; import com.zioinfo.cms.uiws.auth.TwoFactorService; import com.zioinfo.cms.uiws.common.UiwsApiException; import com.zioinfo.cms.uiws.common.UiwsErrorCode; +import com.zioinfo.cms.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; import java.util.HashMap; import java.util.Map; @@ -17,14 +26,20 @@ import java.util.Map; * - UIWS 2FA 레이어(이메일 코드) 추가: 2FA on 이면 1차 통과 후 verify-token + 이메일코드 발급. * 실패 누적 max-login-fail 회 시 계정 잠금. */ +@Slf4j @Service @RequiredArgsConstructor public class AuthService { + private static final SecureRandom RANDOM = new SecureRandom(); + /** 임시 비밀번호 문자셋(혼동 문자 0/O/1/l/I 제외). */ + private static final String TMP_PW_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789@#$%"; + private final UserMapper userMapper; private final PasswordEncoder passwordEncoder; private final JwtUtil jwtUtil; private final TwoFactorService twoFactorService; + private final MailSender mailSender; /** * 1차 로그인. 2FA 활성 시 verify-token + 이메일코드 흐름으로 분기, @@ -43,6 +58,10 @@ public class AuthService { if (user == null || !user.isActive()) { throw new RuntimeException("ERR-AUTH-001: 존재하지 않거나 비활성 계정"); } + // 회원가입 승인 게이트(로그인 보조 이식): 미승인 계정은 비번 일치 전에 차단. + if (Boolean.FALSE.equals(user.getApproved())) { + throw new RuntimeException("ERR-AUTH-003: 승인 대기 중인 계정입니다. 관리자 승인 후 이용 가능합니다."); + } if (!passwordEncoder.matches(password, user.getPasswordHash())) { // 2FA 활성 시 실패 누적/잠금. 비활성 시 기존 동작(메시지만) 유지. if (twoFactorService.isEnabled()) { @@ -81,4 +100,100 @@ public class AuthService { m.put("displayName", u != null ? u.getDisplayName() : username); return m; } + + // ── 로그인 보조 이식: 회원가입 / 아이디찾기 / 비밀번호 초기화 ─────────────────── + + /** + * 운영자 회원가입(승인 대기). username/email 중복 검사 후 approved=false·role=VIEWER 로 INSERT. + * 비밀번호는 BCrypt 저장. 활성/승인 전까지 로그인 차단(AuthService.login 의 승인 게이트). + */ + @Transactional + public AuthHelperResult signup(SignupRequest req) { + if (req.username() == null || req.username().isBlank() + || req.password() == null || req.password().length() < 4 + || req.email() == null || req.email().isBlank()) { + return new AuthHelperResult(false, "아이디·비밀번호(4자 이상)·이메일은 필수입니다."); + } + if (userMapper.countByUsername(req.username()) > 0) { + return new AuthHelperResult(false, "이미 사용 중인 아이디입니다."); + } + if (userMapper.countByEmail(req.email()) > 0) { + return new AuthHelperResult(false, "이미 등록된 이메일입니다."); + } + CmsUser u = new CmsUser(); + u.setUsername(req.username()); + u.setPasswordHash(passwordEncoder.encode(req.password())); + u.setDisplayName(req.displayName() != null && !req.displayName().isBlank() + ? req.displayName() : req.username()); + u.setEmail(req.email()); + userMapper.signup(u); + log.info("[auth-helper] signup pending approval: username={}", req.username()); + return new AuthHelperResult(true, "가입 신청이 접수되었습니다. 관리자 승인 후 로그인할 수 있습니다."); + } + + /** + * 아이디 찾기: 표시명+이메일 동시 일치 운영자 1건 조회. username 은 부분 마스킹 후 반환. + * 미발견 시 found=false(원문 username 절대 미노출). + */ + public FindIdResponse findId(FindIdRequest req) { + if (req.displayName() == null || req.displayName().isBlank() + || req.email() == null || req.email().isBlank()) { + return new FindIdResponse(false, ""); + } + CmsUser u = userMapper.findByDisplayNameAndEmail(req.displayName(), req.email()); + if (u == null) { + return new FindIdResponse(false, ""); + } + return new FindIdResponse(true, maskUsername(u.getUsername())); + } + + /** + * 비밀번호 초기화: username+email 일치 검증 → 임시비번 생성·BCrypt 저장·잠금/실패카운트 해제. + * 임시비번은 메일(미설정 시 LogMailSender 로그)로만 전달. API 응답·로그 메시지에 비번 미노출. + * 대상 미존재여도 success=true(계정 열거 방지). + */ + @Transactional + public AuthHelperResult resetPassword(ResetPasswordRequest req) { + final String okMsg = "임시 비밀번호를 등록된 이메일로 발송했습니다. 메일을 확인하세요."; + if (req.username() == null || req.username().isBlank() + || req.email() == null || req.email().isBlank()) { + return new AuthHelperResult(false, "아이디와 이메일을 모두 입력하세요."); + } + CmsUser u = userMapper.findByUsernameAndEmail(req.username(), req.email()); + if (u == null) { + // 존재 여부 누설 방지 — 동일 성공 메시지 반환(실제 발송 없음). + log.info("[auth-helper] reset-password no match (suppressed): username={}", req.username()); + return new AuthHelperResult(true, okMsg); + } + String tempPw = generateTempPassword(); + userMapper.updatePasswordHash(req.username(), passwordEncoder.encode(tempPw)); + + String subject = "[GUARDiA CMS] 임시 비밀번호 안내"; + String body = String.format( + "안녕하세요 %s 님,\n임시 비밀번호는 [%s] 입니다.\n로그인 후 즉시 비밀번호를 변경하세요.", + u.getDisplayName() != null ? u.getDisplayName() : u.getUsername(), tempPw); + // 메일 본문에만 임시비번 포함. mailSender 미설정 환경은 LogMailSender 폴백(서버 로그). + mailSender.send(u.getEmail(), subject, body); + log.info("[auth-helper] reset-password issued temp pw (sent via mail/log): username={}", req.username()); + return new AuthHelperResult(true, okMsg); + } + + private static String generateTempPassword() { + StringBuilder sb = new StringBuilder(10); + for (int i = 0; i < 10; i++) { + sb.append(TMP_PW_CHARS.charAt(RANDOM.nextInt(TMP_PW_CHARS.length()))); + } + return sb.toString(); + } + + /** username 부분 마스킹: 앞 2자만 노출(예: admin → ad***). 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(Math.max(1, username.length() - 2)); + } } diff --git a/backend/src/main/java/com/zioinfo/cms/auth/CmsUser.java b/backend/src/main/java/com/zioinfo/cms/auth/CmsUser.java index f84168b..44035e8 100644 --- a/backend/src/main/java/com/zioinfo/cms/auth/CmsUser.java +++ b/backend/src/main/java/com/zioinfo/cms/auth/CmsUser.java @@ -32,4 +32,8 @@ public class CmsUser { private Boolean locked; /** TOTP 시크릿(UIWS OTP 경로 대비, 현재 이메일 흐름에서는 미사용). */ private String otpSecret; + + // ── 로그인 보조 이식(회원가입 승인 게이트) cms_user.approved ─────────────── + /** 회원가입 승인 여부(기본 true). 신규 가입자는 false → SUPERADMIN 승인 전 로그인 차단. */ + private Boolean approved; } diff --git a/backend/src/main/java/com/zioinfo/cms/auth/dto/AuthHelperResult.java b/backend/src/main/java/com/zioinfo/cms/auth/dto/AuthHelperResult.java new file mode 100644 index 0000000..e9ecde4 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/auth/dto/AuthHelperResult.java @@ -0,0 +1,11 @@ +package com.zioinfo.cms.auth.dto; + +/** + * 로그인 보조 이식 — 회원가입/비밀번호초기화 공통 결과 DTO. + * 항상 일반 메시지만 반환(임시비번·존재여부 등 민감정보 미포함, 자격증명 보호 불변규칙). + * 보안상 비밀번호 초기화는 대상 미존재 시에도 success=true(열거 공격 방지). + */ +public record AuthHelperResult( + boolean success, + String message) { +} diff --git a/backend/src/main/java/com/zioinfo/cms/auth/dto/FindIdRequest.java b/backend/src/main/java/com/zioinfo/cms/auth/dto/FindIdRequest.java new file mode 100644 index 0000000..1b19bbf --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/auth/dto/FindIdRequest.java @@ -0,0 +1,11 @@ +package com.zioinfo.cms.auth.dto; + +/** + * 로그인 보조 이식 — 아이디 찾기 요청 DTO. + * 표시명(displayName) + 이메일(email) 동시 일치하는 운영자 계정을 조회. + * 응답의 username 은 마스킹하여 반환(자격증명 보호). + */ +public record FindIdRequest( + String displayName, + String email) { +} diff --git a/backend/src/main/java/com/zioinfo/cms/auth/dto/FindIdResponse.java b/backend/src/main/java/com/zioinfo/cms/auth/dto/FindIdResponse.java new file mode 100644 index 0000000..5cae9a2 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/auth/dto/FindIdResponse.java @@ -0,0 +1,11 @@ +package com.zioinfo.cms.auth.dto; + +/** + * 로그인 보조 이식 — 아이디 찾기 응답 DTO. + * found=true 이면 maskedUsername(예: ad***) 동봉. 미발견이어도 동일 shape(존재 여부 누설 최소화). + * 원본 username 전체는 절대 노출하지 않는다(부분 마스킹만). + */ +public record FindIdResponse( + boolean found, + String maskedUsername) { +} diff --git a/backend/src/main/java/com/zioinfo/cms/auth/dto/ResetPasswordRequest.java b/backend/src/main/java/com/zioinfo/cms/auth/dto/ResetPasswordRequest.java new file mode 100644 index 0000000..bdfd313 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/auth/dto/ResetPasswordRequest.java @@ -0,0 +1,11 @@ +package com.zioinfo.cms.auth.dto; + +/** + * 로그인 보조 이식 — 비밀번호 초기화 요청 DTO. + * username + email 동시 일치 검증 후 임시 비밀번호를 BCrypt 로 저장. + * 임시 비밀번호는 메일(미설정 시 LogMailSender 로그)로만 전달 — API 응답에 절대 미포함. + */ +public record ResetPasswordRequest( + String username, + String email) { +} diff --git a/backend/src/main/java/com/zioinfo/cms/auth/dto/SignupRequest.java b/backend/src/main/java/com/zioinfo/cms/auth/dto/SignupRequest.java new file mode 100644 index 0000000..0ee7fd6 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/auth/dto/SignupRequest.java @@ -0,0 +1,13 @@ +package com.zioinfo.cms.auth.dto; + +/** + * 로그인 보조 이식 — 운영자 회원가입 요청 DTO. + * 대상: CMS 관리자/운영자 계정(cms_user). UGC 고객회원(cms_member)과 구분. + * 가입 결과는 승인 대기(approved=false) 상태로 INSERT → SUPERADMIN 승인 전 로그인 차단. + */ +public record SignupRequest( + String username, + String password, + String displayName, + String email) { +} diff --git a/backend/src/main/java/com/zioinfo/cms/auth/mapper/UserMapper.java b/backend/src/main/java/com/zioinfo/cms/auth/mapper/UserMapper.java index 8064ff7..2891b26 100644 --- a/backend/src/main/java/com/zioinfo/cms/auth/mapper/UserMapper.java +++ b/backend/src/main/java/com/zioinfo/cms/auth/mapper/UserMapper.java @@ -31,4 +31,30 @@ public interface UserMapper { /** 관리자 잠금 해제(실패 카운트/잠금 초기화). */ int unlock(@Param("username") String username); + + // ── 로그인 보조 이식: 회원가입 / 아이디찾기 / 비밀번호 초기화 ─────────────── + + /** username 존재 여부(회원가입 중복 검사). */ + int countByUsername(@Param("username") String username); + + /** email 존재 여부(회원가입 중복 검사 — 운영자 계정 한정). */ + int countByEmail(@Param("email") String email); + + /** + * 회원가입(승인 대기). approved=false·is_active=true·role=VIEWER 고정. + * 관리자 화면에서 활성/승인 전까지 로그인 차단. + */ + int signup(CmsUser user); + + /** 아이디찾기: 표시명(display_name)+이메일 일치 운영자 1건. */ + CmsUser findByDisplayNameAndEmail(@Param("displayName") String displayName, + @Param("email") String email); + + /** 비밀번호 초기화 대상 검증: username+email 동시 일치 운영자 1건. */ + CmsUser findByUsernameAndEmail(@Param("username") String username, + @Param("email") String email); + + /** 임시 비밀번호 적용 + 잠금/실패카운트 해제(초기화 시). */ + int updatePasswordHash(@Param("username") String username, + @Param("passwordHash") String passwordHash); } diff --git a/backend/src/main/java/com/zioinfo/cms/config/SecurityConfig.java b/backend/src/main/java/com/zioinfo/cms/config/SecurityConfig.java index 126987c..9643955 100644 --- a/backend/src/main/java/com/zioinfo/cms/config/SecurityConfig.java +++ b/backend/src/main/java/com/zioinfo/cms/config/SecurityConfig.java @@ -45,6 +45,9 @@ public class SecurityConfig { .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/cms/auth/**").permitAll() + // 로그인 보조 이식(회원가입/아이디찾기/비번초기화) — 로그인 전 무인증. + // 회원가입은 승인대기로 INSERT, 비번초기화는 임시비번을 메일/로그로만 전달(응답 무노출). + .requestMatchers("/api/auth/signup", "/api/auth/find-id", "/api/auth/reset-password").permitAll() .requestMatchers("/actuator/health").permitAll() .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/api/cms/docs/**", "/api/cms/swagger/**").permitAll() @@ -62,6 +65,12 @@ public class SecurityConfig { .requestMatchers("/api/admin/audit").hasAnyRole("SUPERADMIN", "EDITOR") .requestMatchers("/api/admin/**").hasRole("SUPERADMIN") + // UIWS PublicLookup(가입화면 부서/회사 셀렉트박스용 공개 조회) — 로그인 전 GET 무인증. + // 조회 전용(부서명/회사명만) — 민감정보·자격증명 미포함. + .requestMatchers(HttpMethod.GET, "/api/public/**").permitAll() + // UIWS system(시스템관리·권한관리) 이식 — SUPERADMIN 전용. + .requestMatchers("/api/system/**").hasRole("SUPERADMIN") + // 콘텐츠/리소스 작성·수정·삭제 — AUTHOR 이상 (게시 승인/발행은 서비스 메서드에서 EDITOR+ 추가 가드) .requestMatchers(HttpMethod.POST, "/api/cms/**").hasAnyRole("SUPERADMIN", "EDITOR", "AUTHOR") .requestMatchers(HttpMethod.PUT, "/api/cms/**").hasAnyRole("SUPERADMIN", "EDITOR", "AUTHOR") diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/common/UiwsErrorCode.java b/backend/src/main/java/com/zioinfo/cms/uiws/common/UiwsErrorCode.java index cc9478f..6fe5409 100644 --- a/backend/src/main/java/com/zioinfo/cms/uiws/common/UiwsErrorCode.java +++ b/backend/src/main/java/com/zioinfo/cms/uiws/common/UiwsErrorCode.java @@ -36,7 +36,19 @@ public enum UiwsErrorCode { // auth (2FA) VERIFY_TOKEN_INVALID("ERR-UIWS-2FA-401", "2차 검증 토큰이 유효하지 않거나 만료되었습니다."), VERIFY_CODE_INVALID("ERR-UIWS-2FA-401C", "인증 코드가 올바르지 않거나 만료되었습니다."), - ACCOUNT_LOCKED("ERR-UIWS-2FA-LOCK", "로그인 실패 횟수 초과로 계정이 잠겼습니다. 관리자에게 문의하세요."); + ACCOUNT_LOCKED("ERR-UIWS-2FA-LOCK", "로그인 실패 횟수 초과로 계정이 잠겼습니다. 관리자에게 문의하세요."), + + // 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 message; diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/CodeController.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/CodeController.java new file mode 100644 index 0000000..a7a0f37 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/CodeController.java @@ -0,0 +1,58 @@ +package com.zioinfo.cms.uiws.system.controller; + +import com.zioinfo.cms.common.ApiResponse; +import com.zioinfo.cms.uiws.system.dto.CodeGrpDetailDto; +import com.zioinfo.cms.uiws.system.dto.CodeGrpDto; +import com.zioinfo.cms.uiws.system.dto.CodeGrpSaveDto; +import com.zioinfo.cms.uiws.system.dto.CodeValueDto; +import com.zioinfo.cms.uiws.system.dto.PageResponse; +import com.zioinfo.cms.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> 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> getValues(@PathVariable("grpCd") String grpCd) { + return ApiResponse.ok(codeService.getValues(grpCd)); + } + + @GetMapping("/{grpCd}") + public ApiResponse getGroup(@PathVariable("grpCd") String grpCd) { + return ApiResponse.ok(codeService.getGroup(grpCd)); + } + + @PostMapping + public ApiResponse create(@Valid @RequestBody CodeGrpSaveDto dto) { + return ApiResponse.ok(codeService.create(dto)); + } + + @PutMapping("/{grpCd}") + public ApiResponse update(@PathVariable("grpCd") String grpCd, @Valid @RequestBody CodeGrpSaveDto dto) { + return ApiResponse.ok(codeService.update(grpCd, dto)); + } + + @DeleteMapping("/{grpCd}") + public ApiResponse delete(@PathVariable("grpCd") String grpCd) { + codeService.delete(grpCd); + return ApiResponse.ok(null); + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/CompanyController.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/CompanyController.java new file mode 100644 index 0000000..33f14b6 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/CompanyController.java @@ -0,0 +1,56 @@ +package com.zioinfo.cms.uiws.system.controller; + +import com.zioinfo.cms.common.ApiResponse; +import com.zioinfo.cms.uiws.system.dto.CompanyDto; +import com.zioinfo.cms.uiws.system.dto.CompanySaveDto; +import com.zioinfo.cms.uiws.system.dto.IdsRequest; +import com.zioinfo.cms.uiws.system.dto.PageResponse; +import com.zioinfo.cms.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> 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> search(@RequestParam(required = false) String keyword) { + return ApiResponse.ok(companyService.searchPopup(keyword)); + } + + @PostMapping + public ApiResponse create(@Valid @RequestBody CompanySaveDto dto) { + return ApiResponse.ok(companyService.create(dto)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable("id") String id) { + return ApiResponse.ok(companyService.get(id)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable("id") String id, @Valid @RequestBody CompanySaveDto dto) { + return ApiResponse.ok(companyService.update(id, dto)); + } + + @DeleteMapping + public ApiResponse delete(@Valid @RequestBody IdsRequest req) { + companyService.delete(req.ids()); + return ApiResponse.ok(null); + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/DeptController.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/DeptController.java new file mode 100644 index 0000000..7e5da69 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/DeptController.java @@ -0,0 +1,61 @@ +package com.zioinfo.cms.uiws.system.controller; + +import com.zioinfo.cms.common.ApiResponse; +import com.zioinfo.cms.uiws.system.dto.DeptDto; +import com.zioinfo.cms.uiws.system.dto.DeptSaveDto; +import com.zioinfo.cms.uiws.system.dto.DeptTreeDto; +import com.zioinfo.cms.uiws.system.dto.PageResponse; +import com.zioinfo.cms.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> 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> search(@RequestParam(required = false) String keyword) { + return ApiResponse.ok(deptService.searchPopup(keyword)); + } + + @GetMapping("/tree") + public ApiResponse> tree() { + return ApiResponse.ok(deptService.tree()); + } + + @PostMapping + public ApiResponse create(@Valid @RequestBody DeptSaveDto dto) { + return ApiResponse.ok(deptService.create(dto)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable("id") String id) { + return ApiResponse.ok(deptService.get(id)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable("id") String id, @Valid @RequestBody DeptSaveDto dto) { + return ApiResponse.ok(deptService.update(id, dto)); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable("id") String id) { + deptService.delete(id); + return ApiResponse.ok(null); + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/DeptRoleController.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/DeptRoleController.java new file mode 100644 index 0000000..6c0975f --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/DeptRoleController.java @@ -0,0 +1,39 @@ +package com.zioinfo.cms.uiws.system.controller; + +import com.zioinfo.cms.common.ApiResponse; +import com.zioinfo.cms.uiws.system.dto.DeptUserRoleDto; +import com.zioinfo.cms.uiws.system.dto.PageResponse; +import com.zioinfo.cms.uiws.system.dto.RoleIdsRequest; +import com.zioinfo.cms.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> 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 grant(@PathVariable("deptId") String deptId, @Valid @RequestBody RoleIdsRequest req) { + deptRoleService.grant(deptId, req.roleIds()); + return ApiResponse.ok(null); + } + + @DeleteMapping + public ApiResponse revoke(@PathVariable("deptId") String deptId, @Valid @RequestBody RoleIdsRequest req) { + deptRoleService.revoke(deptId, req.roleIds()); + return ApiResponse.ok(null); + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/MenuController.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/MenuController.java new file mode 100644 index 0000000..9bc3027 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/MenuController.java @@ -0,0 +1,55 @@ +package com.zioinfo.cms.uiws.system.controller; + +import com.zioinfo.cms.common.ApiResponse; +import com.zioinfo.cms.uiws.system.dto.IdsRequest; +import com.zioinfo.cms.uiws.system.dto.MenuDto; +import com.zioinfo.cms.uiws.system.dto.MenuSaveDto; +import com.zioinfo.cms.uiws.system.dto.PageResponse; +import com.zioinfo.cms.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> list( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "100") int size) { + return ApiResponse.ok(menuService.list(page, size)); + } + + @GetMapping("/search") + public ApiResponse> search(@RequestParam(required = false) String keyword) { + return ApiResponse.ok(menuService.searchPopup(keyword)); + } + + @PostMapping + public ApiResponse create(@Valid @RequestBody MenuSaveDto dto) { + return ApiResponse.ok(menuService.create(dto)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable("id") String id) { + return ApiResponse.ok(menuService.get(id)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable("id") String id, @Valid @RequestBody MenuSaveDto dto) { + return ApiResponse.ok(menuService.update(id, dto)); + } + + @DeleteMapping + public ApiResponse delete(@Valid @RequestBody IdsRequest req) { + menuService.delete(req.ids()); + return ApiResponse.ok(null); + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/ProgramController.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/ProgramController.java new file mode 100644 index 0000000..88d0674 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/ProgramController.java @@ -0,0 +1,57 @@ +package com.zioinfo.cms.uiws.system.controller; + +import com.zioinfo.cms.common.ApiResponse; +import com.zioinfo.cms.uiws.system.dto.IdsRequest; +import com.zioinfo.cms.uiws.system.dto.PageResponse; +import com.zioinfo.cms.uiws.system.dto.ProgramDto; +import com.zioinfo.cms.uiws.system.dto.ProgramSaveDto; +import com.zioinfo.cms.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> 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> search(@RequestParam(required = false) String keyword) { + return ApiResponse.ok(programService.searchPopup(keyword)); + } + + @PostMapping + public ApiResponse create(@Valid @RequestBody ProgramSaveDto dto) { + return ApiResponse.ok(programService.create(dto)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable("id") String id) { + return ApiResponse.ok(programService.get(id)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable("id") String id, @Valid @RequestBody ProgramSaveDto dto) { + return ApiResponse.ok(programService.update(id, dto)); + } + + @DeleteMapping + public ApiResponse delete(@Valid @RequestBody IdsRequest req) { + programService.delete(req.ids()); + return ApiResponse.ok(null); + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/PublicLookupController.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/PublicLookupController.java new file mode 100644 index 0000000..9bbca9a --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/PublicLookupController.java @@ -0,0 +1,29 @@ +package com.zioinfo.cms.uiws.system.controller; + +import com.zioinfo.cms.common.ApiResponse; +import com.zioinfo.cms.uiws.system.dto.PublicCompanyDto; +import com.zioinfo.cms.uiws.system.dto.PublicDeptDto; +import com.zioinfo.cms.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> depts() { + return ApiResponse.ok(publicLookupService.depts()); + } + + @GetMapping("/companies") + public ApiResponse> companies() { + return ApiResponse.ok(publicLookupService.companies()); + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/RoleController.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/RoleController.java new file mode 100644 index 0000000..574d32f --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/RoleController.java @@ -0,0 +1,49 @@ +package com.zioinfo.cms.uiws.system.controller; + +import com.zioinfo.cms.common.ApiResponse; +import com.zioinfo.cms.uiws.system.dto.IdsRequest; +import com.zioinfo.cms.uiws.system.dto.PageResponse; +import com.zioinfo.cms.uiws.system.dto.RoleDto; +import com.zioinfo.cms.uiws.system.dto.RoleSaveDto; +import com.zioinfo.cms.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> 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 create(@Valid @RequestBody RoleSaveDto dto) { + return ApiResponse.ok(roleService.create(dto)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable("id") String id) { + return ApiResponse.ok(roleService.get(id)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable("id") String id, @Valid @RequestBody RoleSaveDto dto) { + return ApiResponse.ok(roleService.update(id, dto)); + } + + @DeleteMapping + public ApiResponse delete(@Valid @RequestBody IdsRequest req) { + roleService.delete(req.ids()); + return ApiResponse.ok(null); + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/RoleMenuController.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/RoleMenuController.java new file mode 100644 index 0000000..6a76527 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/RoleMenuController.java @@ -0,0 +1,39 @@ +package com.zioinfo.cms.uiws.system.controller; + +import com.zioinfo.cms.common.ApiResponse; +import com.zioinfo.cms.uiws.system.dto.PageResponse; +import com.zioinfo.cms.uiws.system.dto.RoleDto; +import com.zioinfo.cms.uiws.system.dto.RoleMenuDto; +import com.zioinfo.cms.uiws.system.dto.RoleMenuSaveRequest; +import com.zioinfo.cms.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> 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> getRoleMenus(@PathVariable("roleId") String roleId) { + return ApiResponse.ok(roleMenuService.getRoleMenus(roleId)); + } + + @PutMapping("/api/system/roles/{roleId}/menus") + public ApiResponse saveRoleMenus(@PathVariable("roleId") String roleId, @Valid @RequestBody RoleMenuSaveRequest req) { + roleMenuService.saveRoleMenus(roleId, req.menus()); + return ApiResponse.ok(null); + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/UserController.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/UserController.java new file mode 100644 index 0000000..51a48bb --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/controller/UserController.java @@ -0,0 +1,89 @@ +package com.zioinfo.cms.uiws.system.controller; + +import com.zioinfo.cms.common.ApiResponse; +import com.zioinfo.cms.uiws.system.dto.CheckIdResponse; +import com.zioinfo.cms.uiws.system.dto.IdsRequest; +import com.zioinfo.cms.uiws.system.dto.PageResponse; +import com.zioinfo.cms.uiws.system.dto.UserDto; +import com.zioinfo.cms.uiws.system.dto.UserSaveDto; +import com.zioinfo.cms.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> 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> search( + @RequestParam(required = false) String keyword, + @RequestParam(required = false) String deptId) { + return ApiResponse.ok(userService.searchPopup(keyword, deptId)); + } + + @GetMapping("/check-id") + public ApiResponse checkId(@RequestParam String userId) { + return ApiResponse.ok(userService.checkId(userId)); + } + + @PostMapping + public ApiResponse create(@Valid @RequestBody UserSaveDto dto) { + return ApiResponse.ok(userService.create(dto)); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable("id") String id) { + return ApiResponse.ok(userService.get(id)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable("id") String id, @Valid @RequestBody UserSaveDto dto) { + return ApiResponse.ok(userService.update(id, dto)); + } + + @DeleteMapping + public ApiResponse delete(@Valid @RequestBody IdsRequest req) { + userService.delete(req.ids()); + return ApiResponse.ok(null); + } + + @PostMapping("/{id}/reset-pw") + public ApiResponse resetPassword(@PathVariable("id") String id) { + userService.resetPassword(id); + return ApiResponse.ok(null); + } + + @PostMapping("/{id}/unlock") + public ApiResponse unlock(@PathVariable("id") String id) { + userService.unlock(id); + return ApiResponse.ok(null); + } + + @PostMapping("/{id}/approve") + public ApiResponse approve(@PathVariable("id") String id) { + userService.approve(id); + return ApiResponse.ok(null); + } + + @PostMapping("/{id}/revoke-approval") + public ApiResponse revokeApproval(@PathVariable("id") String id) { + userService.revokeApproval(id); + return ApiResponse.ok(null); + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CheckIdResponse.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CheckIdResponse.java new file mode 100644 index 0000000..ac48dea --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CheckIdResponse.java @@ -0,0 +1,4 @@ +package com.zioinfo.cms.uiws.system.dto; + +/** { available: boolean } */ +public record CheckIdResponse(boolean available) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CodeGrpDetailDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CodeGrpDetailDto.java new file mode 100644 index 0000000..bd1dcf8 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CodeGrpDetailDto.java @@ -0,0 +1,6 @@ +package com.zioinfo.cms.uiws.system.dto; + +import java.util.List; + +/** { grpCd, grpNm, useYn, values: CodeValueDto[] } */ +public record CodeGrpDetailDto(String grpCd, String grpNm, String useYn, List values) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CodeGrpDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CodeGrpDto.java new file mode 100644 index 0000000..87a4358 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CodeGrpDto.java @@ -0,0 +1,4 @@ +package com.zioinfo.cms.uiws.system.dto; + +/** { grpCd, grpNm, useYn } */ +public record CodeGrpDto(String grpCd, String grpNm, String useYn) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CodeGrpSaveDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CodeGrpSaveDto.java new file mode 100644 index 0000000..2032d7f --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CodeGrpSaveDto.java @@ -0,0 +1,11 @@ +package com.zioinfo.cms.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 values) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CodeValueDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CodeValueDto.java new file mode 100644 index 0000000..12a92ac --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CodeValueDto.java @@ -0,0 +1,4 @@ +package com.zioinfo.cms.uiws.system.dto; + +/** { grpCd, codeVal, codeNm, sortOrd, useYn } */ +public record CodeValueDto(String grpCd, String codeVal, String codeNm, Integer sortOrd, String useYn) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CompanyDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CompanyDto.java new file mode 100644 index 0000000..b4322e4 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CompanyDto.java @@ -0,0 +1,4 @@ +package com.zioinfo.cms.uiws.system.dto; + +/** { companyId, companyNm, bizNo, useYn } */ +public record CompanyDto(String companyId, String companyNm, String bizNo, String useYn) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CompanySaveDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CompanySaveDto.java new file mode 100644 index 0000000..2774f69 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/CompanySaveDto.java @@ -0,0 +1,10 @@ +package com.zioinfo.cms.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) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/DeptDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/DeptDto.java new file mode 100644 index 0000000..ca6ec07 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/DeptDto.java @@ -0,0 +1,4 @@ +package com.zioinfo.cms.uiws.system.dto; + +/** { deptId, deptNm, parentDeptId, sortOrd, useYn } */ +public record DeptDto(String deptId, String deptNm, String parentDeptId, Integer sortOrd, String useYn) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/DeptSaveDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/DeptSaveDto.java new file mode 100644 index 0000000..4cd41f0 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/DeptSaveDto.java @@ -0,0 +1,11 @@ +package com.zioinfo.cms.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) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/DeptTreeDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/DeptTreeDto.java new file mode 100644 index 0000000..b6f386a --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/DeptTreeDto.java @@ -0,0 +1,8 @@ +package com.zioinfo.cms.uiws.system.dto; + +import java.util.List; + +/** 부서 계층 트리 노드. children 은 sortOrd→deptId 순. */ +public record DeptTreeDto( + String deptId, String deptNm, String parentDeptId, + Integer sortOrd, String useYn, List children) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/DeptUserRoleDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/DeptUserRoleDto.java new file mode 100644 index 0000000..f2e2983 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/DeptUserRoleDto.java @@ -0,0 +1,6 @@ +package com.zioinfo.cms.uiws.system.dto; + +import java.util.List; + +/** { userId, userNm, roleIds } — 부서 사용자별 부여 권한. */ +public record DeptUserRoleDto(String userId, String userNm, List roleIds) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/IdsRequest.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/IdsRequest.java new file mode 100644 index 0000000..56cc00a --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/IdsRequest.java @@ -0,0 +1,7 @@ +package com.zioinfo.cms.uiws.system.dto; + +import jakarta.validation.constraints.NotEmpty; +import java.util.List; + +/** 다중삭제 공통 본문: { ids: string[] } */ +public record IdsRequest(@NotEmpty(message = "ids는 필수입니다.") List ids) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/MenuDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/MenuDto.java new file mode 100644 index 0000000..12a715c --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/MenuDto.java @@ -0,0 +1,6 @@ +package com.zioinfo.cms.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) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/MenuSaveDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/MenuSaveDto.java new file mode 100644 index 0000000..45d073b --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/MenuSaveDto.java @@ -0,0 +1,10 @@ +package com.zioinfo.cms.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) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/PageResponse.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/PageResponse.java new file mode 100644 index 0000000..4147ff4 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/PageResponse.java @@ -0,0 +1,18 @@ +package com.zioinfo.cms.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( + List content, + long total, + int page, + int size +) { + public static PageResponse of(List content, long total, int page, int size) { + return new PageResponse<>(content, total, page, size); + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/ProgramDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/ProgramDto.java new file mode 100644 index 0000000..837fd75 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/ProgramDto.java @@ -0,0 +1,6 @@ +package com.zioinfo.cms.uiws.system.dto; + +/** { programId, programNm, programType, programUrl, category, useYn } */ +public record ProgramDto( + String programId, String programNm, String programType, + String programUrl, String category, String useYn) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/ProgramSaveDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/ProgramSaveDto.java new file mode 100644 index 0000000..c19dac0 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/ProgramSaveDto.java @@ -0,0 +1,11 @@ +package com.zioinfo.cms.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) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/PublicCompanyDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/PublicCompanyDto.java new file mode 100644 index 0000000..c668b6a --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/PublicCompanyDto.java @@ -0,0 +1,4 @@ +package com.zioinfo.cms.uiws.system.dto; + +/** 가입 화면 공개 조회용 거래처: { companyId, companyNm } */ +public record PublicCompanyDto(String companyId, String companyNm) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/PublicDeptDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/PublicDeptDto.java new file mode 100644 index 0000000..9095a74 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/PublicDeptDto.java @@ -0,0 +1,4 @@ +package com.zioinfo.cms.uiws.system.dto; + +/** 가입 화면 공개 조회용 부서: { deptId, deptNm } */ +public record PublicDeptDto(String deptId, String deptNm) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleDto.java new file mode 100644 index 0000000..e8a3483 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleDto.java @@ -0,0 +1,4 @@ +package com.zioinfo.cms.uiws.system.dto; + +/** { roleId, roleNm, roleDesc, useYn } */ +public record RoleDto(String roleId, String roleNm, String roleDesc, String useYn) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleIdsRequest.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleIdsRequest.java new file mode 100644 index 0000000..620cc96 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleIdsRequest.java @@ -0,0 +1,7 @@ +package com.zioinfo.cms.uiws.system.dto; + +import jakarta.validation.constraints.NotEmpty; +import java.util.List; + +/** 부서권한 부여/삭제 본문: { roleIds: string[] } */ +public record RoleIdsRequest(@NotEmpty(message = "roleIds는 필수입니다.") List roleIds) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleMenuDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleMenuDto.java new file mode 100644 index 0000000..cd4b62a --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleMenuDto.java @@ -0,0 +1,4 @@ +package com.zioinfo.cms.uiws.system.dto; + +/** { menuId, menuNm, readYn, writeYn } */ +public record RoleMenuDto(String menuId, String menuNm, String readYn, String writeYn) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleMenuSaveRequest.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleMenuSaveRequest.java new file mode 100644 index 0000000..5d3b4fd --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleMenuSaveRequest.java @@ -0,0 +1,6 @@ +package com.zioinfo.cms.uiws.system.dto; + +import java.util.List; + +/** 권한별 메뉴 매핑 저장 본문: { menus: RoleMenuDto[] } */ +public record RoleMenuSaveRequest(List menus) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleSaveDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleSaveDto.java new file mode 100644 index 0000000..8bcb235 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/RoleSaveDto.java @@ -0,0 +1,10 @@ +package com.zioinfo.cms.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) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/UserDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/UserDto.java new file mode 100644 index 0000000..1892cc4 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/UserDto.java @@ -0,0 +1,7 @@ +package com.zioinfo.cms.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) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/UserSaveDto.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/UserSaveDto.java new file mode 100644 index 0000000..c175aab --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/dto/UserSaveDto.java @@ -0,0 +1,13 @@ +package com.zioinfo.cms.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) {} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/CodeMapper.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/CodeMapper.java new file mode 100644 index 0000000..524cc99 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/CodeMapper.java @@ -0,0 +1,23 @@ +package com.zioinfo.cms.uiws.system.mapper; + +import com.zioinfo.cms.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 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 findValuesByGrp(@Param("grpCd") String grpCd); + List findActiveValuesByGrp(@Param("grpCd") String grpCd); + int insertValue(SysCode code); + int deleteValuesByGrp(@Param("grpCd") String grpCd); +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/CompanyMapper.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/CompanyMapper.java new file mode 100644 index 0000000..22c96be --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/CompanyMapper.java @@ -0,0 +1,22 @@ +package com.zioinfo.cms.uiws.system.mapper; + +import com.zioinfo.cms.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 search(@Param("keyword") String keyword, + @Param("offset") int offset, @Param("size") int size); + long countSearch(@Param("keyword") String keyword); + List searchActive(@Param("keyword") String keyword); + List findActiveOrdered(); + List 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); +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/DeptMapper.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/DeptMapper.java new file mode 100644 index 0000000..1536329 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/DeptMapper.java @@ -0,0 +1,23 @@ +package com.zioinfo.cms.uiws.system.mapper; + +import com.zioinfo.cms.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 search(@Param("keyword") String keyword, + @Param("offset") int offset, @Param("size") int size); + long countSearch(@Param("keyword") String keyword); + List searchActive(@Param("keyword") String keyword); + List findActiveOrdered(); + List 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); +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/DeptRoleMapper.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/DeptRoleMapper.java new file mode 100644 index 0000000..3d4c3f5 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/DeptRoleMapper.java @@ -0,0 +1,16 @@ +package com.zioinfo.cms.uiws.system.mapper; + +import com.zioinfo.cms.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 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); +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/MenuMapper.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/MenuMapper.java new file mode 100644 index 0000000..4c62ed1 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/MenuMapper.java @@ -0,0 +1,22 @@ +package com.zioinfo.cms.uiws.system.mapper; + +import com.zioinfo.cms.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 searchAll(@Param("offset") int offset, @Param("size") int size); + long countAll(); + List search(@Param("keyword") String keyword); + List 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); +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/ProgramMapper.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/ProgramMapper.java new file mode 100644 index 0000000..6ee6a1d --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/ProgramMapper.java @@ -0,0 +1,20 @@ +package com.zioinfo.cms.uiws.system.mapper; + +import com.zioinfo.cms.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 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 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); +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/RoleMapper.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/RoleMapper.java new file mode 100644 index 0000000..46d0d88 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/RoleMapper.java @@ -0,0 +1,19 @@ +package com.zioinfo.cms.uiws.system.mapper; + +import com.zioinfo.cms.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 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); +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/RoleMenuMapper.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/RoleMenuMapper.java new file mode 100644 index 0000000..f885933 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/RoleMenuMapper.java @@ -0,0 +1,15 @@ +package com.zioinfo.cms.uiws.system.mapper; + +import com.zioinfo.cms.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 findByRoleId(@Param("roleId") String roleId); + boolean existsByMenuId(@Param("menuId") String menuId); + int insert(SysRoleMenu rm); + int deleteByRoleId(@Param("roleId") String roleId); +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/SysUserMapper.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/SysUserMapper.java new file mode 100644 index 0000000..fe6cd00 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/mapper/SysUserMapper.java @@ -0,0 +1,20 @@ +package com.zioinfo.cms.uiws.system.mapper; + +import com.zioinfo.cms.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 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 searchActive(@Param("keyword") String keyword, @Param("deptId") String deptId); + List 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); +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysCode.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysCode.java new file mode 100644 index 0000000..03d2b88 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysCode.java @@ -0,0 +1,18 @@ +package com.zioinfo.cms.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; +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysCodeGrp.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysCodeGrp.java new file mode 100644 index 0000000..4ad7814 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysCodeGrp.java @@ -0,0 +1,16 @@ +package com.zioinfo.cms.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; +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysCompany.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysCompany.java new file mode 100644 index 0000000..f0ca2f0 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysCompany.java @@ -0,0 +1,17 @@ +package com.zioinfo.cms.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; +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysDept.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysDept.java new file mode 100644 index 0000000..f71a305 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysDept.java @@ -0,0 +1,18 @@ +package com.zioinfo.cms.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; +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysMenu.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysMenu.java new file mode 100644 index 0000000..dd97f6d --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysMenu.java @@ -0,0 +1,20 @@ +package com.zioinfo.cms.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; +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysProgram.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysProgram.java new file mode 100644 index 0000000..7caa4bb --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysProgram.java @@ -0,0 +1,19 @@ +package com.zioinfo.cms.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; +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysRole.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysRole.java new file mode 100644 index 0000000..0ec2b80 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysRole.java @@ -0,0 +1,17 @@ +package com.zioinfo.cms.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; +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysRoleMenu.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysRoleMenu.java new file mode 100644 index 0000000..2959259 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysRoleMenu.java @@ -0,0 +1,17 @@ +package com.zioinfo.cms.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; +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysUser.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysUser.java new file mode 100644 index 0000000..b080233 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/model/SysUser.java @@ -0,0 +1,29 @@ +package com.zioinfo.cms.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; +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/service/CodeService.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/CodeService.java new file mode 100644 index 0000000..f805332 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/CodeService.java @@ -0,0 +1,119 @@ +package com.zioinfo.cms.uiws.system.service; + +import com.zioinfo.cms.uiws.common.UiwsApiException; +import com.zioinfo.cms.uiws.common.UiwsErrorCode; +import com.zioinfo.cms.uiws.system.dto.CodeGrpDetailDto; +import com.zioinfo.cms.uiws.system.dto.CodeGrpDto; +import com.zioinfo.cms.uiws.system.dto.CodeGrpSaveDto; +import com.zioinfo.cms.uiws.system.dto.CodeValueDto; +import com.zioinfo.cms.uiws.system.dto.PageResponse; +import com.zioinfo.cms.uiws.system.mapper.CodeMapper; +import com.zioinfo.cms.uiws.system.model.SysCode; +import com.zioinfo.cms.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 listGroups(String keyword, int page, int size) { + List 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 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 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 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; + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/service/CompanyService.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/CompanyService.java new file mode 100644 index 0000000..0563fe6 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/CompanyService.java @@ -0,0 +1,89 @@ +package com.zioinfo.cms.uiws.system.service; + +import com.zioinfo.cms.uiws.common.UiwsApiException; +import com.zioinfo.cms.uiws.common.UiwsErrorCode; +import com.zioinfo.cms.uiws.system.dto.CompanyDto; +import com.zioinfo.cms.uiws.system.dto.CompanySaveDto; +import com.zioinfo.cms.uiws.system.dto.PageResponse; +import com.zioinfo.cms.uiws.system.mapper.CompanyMapper; +import com.zioinfo.cms.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 list(String keyword, int page, int size) { + List 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 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 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; + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/service/DeptRoleService.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/DeptRoleService.java new file mode 100644 index 0000000..9f49490 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/DeptRoleService.java @@ -0,0 +1,61 @@ +package com.zioinfo.cms.uiws.system.service; + +import com.zioinfo.cms.uiws.common.UiwsApiException; +import com.zioinfo.cms.uiws.common.UiwsErrorCode; +import com.zioinfo.cms.uiws.system.dto.DeptUserRoleDto; +import com.zioinfo.cms.uiws.system.dto.PageResponse; +import com.zioinfo.cms.uiws.system.mapper.DeptMapper; +import com.zioinfo.cms.uiws.system.mapper.DeptRoleMapper; +import com.zioinfo.cms.uiws.system.mapper.SysUserMapper; +import com.zioinfo.cms.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 listDeptUsers(String deptId, int page, int size) { + ensureDept(deptId); + List roleIds = deptRoleMapper.findRoleIdsByDeptId(deptId); + List users = userMapper.search(null, deptId, page * size, size); + List 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 roleIds) { + ensureDept(deptId); + String actor = SysActor.id(); + for (String roleId : roleIds) { + deptRoleMapper.insert(deptId, roleId, actor); + } + } + + @Transactional + public void revoke(String deptId, List 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); + } + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/service/DeptService.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/DeptService.java new file mode 100644 index 0000000..127519d --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/DeptService.java @@ -0,0 +1,181 @@ +package com.zioinfo.cms.uiws.system.service; + +import com.zioinfo.cms.uiws.common.UiwsApiException; +import com.zioinfo.cms.uiws.common.UiwsErrorCode; +import com.zioinfo.cms.uiws.system.dto.DeptDto; +import com.zioinfo.cms.uiws.system.dto.DeptSaveDto; +import com.zioinfo.cms.uiws.system.dto.DeptTreeDto; +import com.zioinfo.cms.uiws.system.dto.PageResponse; +import com.zioinfo.cms.uiws.system.mapper.DeptMapper; +import com.zioinfo.cms.uiws.system.mapper.SysUserMapper; +import com.zioinfo.cms.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 list(String keyword, int page, int size) { + List 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 searchPopup(String keyword) { + return deptMapper.searchActive(keyword).stream().map(this::toDto).toList(); + } + + /** 부서 계층 트리(루트부터 중첩). sortOrd→deptId 순. */ + @Transactional(readOnly = true) + public List tree() { + List all = deptMapper.findAll(); + Set ids = all.stream().map(SysDept::getDeptId).collect(Collectors.toSet()); + + Map> 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 order = Comparator + .comparing((SysDept d) -> d.getSortOrd() == null ? 0 : d.getSortOrd()) + .thenComparing(SysDept::getDeptId); + + List 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> childrenOf, + Comparator order, Set 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 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 selfAndDescendants(String deptId) { + List all = deptMapper.findAll(); + Map> childrenOf = new HashMap<>(); + for (SysDept d : all) { + if (d.getParentDeptId() != null) { + childrenOf.computeIfAbsent(d.getParentDeptId(), k -> new ArrayList<>()).add(d.getDeptId()); + } + } + Set result = new HashSet<>(); + ArrayList 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; + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/service/MenuService.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/MenuService.java new file mode 100644 index 0000000..5b5e880 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/MenuService.java @@ -0,0 +1,103 @@ +package com.zioinfo.cms.uiws.system.service; + +import com.zioinfo.cms.uiws.common.UiwsApiException; +import com.zioinfo.cms.uiws.common.UiwsErrorCode; +import com.zioinfo.cms.uiws.system.dto.MenuDto; +import com.zioinfo.cms.uiws.system.dto.MenuSaveDto; +import com.zioinfo.cms.uiws.system.dto.PageResponse; +import com.zioinfo.cms.uiws.system.mapper.MenuMapper; +import com.zioinfo.cms.uiws.system.mapper.RoleMenuMapper; +import com.zioinfo.cms.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 list(int page, int size) { + List content = menuMapper.searchAll(page * size, size).stream().map(this::toDto).toList(); + return PageResponse.of(content, menuMapper.countAll(), page, size); + } + + @Transactional(readOnly = true) + public List 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 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; + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/service/ProgramService.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/ProgramService.java new file mode 100644 index 0000000..dbcc80f --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/ProgramService.java @@ -0,0 +1,98 @@ +package com.zioinfo.cms.uiws.system.service; + +import com.zioinfo.cms.uiws.common.UiwsApiException; +import com.zioinfo.cms.uiws.common.UiwsErrorCode; +import com.zioinfo.cms.uiws.system.dto.PageResponse; +import com.zioinfo.cms.uiws.system.dto.ProgramDto; +import com.zioinfo.cms.uiws.system.dto.ProgramSaveDto; +import com.zioinfo.cms.uiws.system.mapper.MenuMapper; +import com.zioinfo.cms.uiws.system.mapper.ProgramMapper; +import com.zioinfo.cms.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 list(String keyword, String programType, int page, int size) { + List 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 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 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; + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/service/PublicLookupService.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/PublicLookupService.java new file mode 100644 index 0000000..9bb6414 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/PublicLookupService.java @@ -0,0 +1,35 @@ +package com.zioinfo.cms.uiws.system.service; + +import com.zioinfo.cms.uiws.system.dto.PublicCompanyDto; +import com.zioinfo.cms.uiws.system.dto.PublicDeptDto; +import com.zioinfo.cms.uiws.system.mapper.CompanyMapper; +import com.zioinfo.cms.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 depts() { + return deptMapper.findActiveOrdered().stream() + .map(d -> new PublicDeptDto(d.getDeptId(), d.getDeptNm())).toList(); + } + + @Transactional(readOnly = true) + public List companies() { + return companyMapper.findActiveOrdered().stream() + .map(c -> new PublicCompanyDto(c.getCompanyId(), c.getCompanyNm())).toList(); + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/service/RoleMenuService.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/RoleMenuService.java new file mode 100644 index 0000000..84f3e74 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/RoleMenuService.java @@ -0,0 +1,91 @@ +package com.zioinfo.cms.uiws.system.service; + +import com.zioinfo.cms.uiws.common.UiwsApiException; +import com.zioinfo.cms.uiws.common.UiwsErrorCode; +import com.zioinfo.cms.uiws.system.dto.PageResponse; +import com.zioinfo.cms.uiws.system.dto.RoleDto; +import com.zioinfo.cms.uiws.system.dto.RoleMenuDto; +import com.zioinfo.cms.uiws.system.mapper.MenuMapper; +import com.zioinfo.cms.uiws.system.mapper.RoleMapper; +import com.zioinfo.cms.uiws.system.mapper.RoleMenuMapper; +import com.zioinfo.cms.uiws.system.model.SysMenu; +import com.zioinfo.cms.uiws.system.model.SysRole; +import com.zioinfo.cms.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 listRoles(int page, int size) { + List 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 getRoleMenus(String roleId) { + ensureRole(roleId); + Map mapped = new LinkedHashMap<>(); + for (SysRoleMenu rm : roleMenuMapper.findByRoleId(roleId)) { + mapped.put(rm.getMenuId(), rm); + } + List 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 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); + } + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/service/RoleService.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/RoleService.java new file mode 100644 index 0000000..f3e653d --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/RoleService.java @@ -0,0 +1,89 @@ +package com.zioinfo.cms.uiws.system.service; + +import com.zioinfo.cms.uiws.common.UiwsApiException; +import com.zioinfo.cms.uiws.common.UiwsErrorCode; +import com.zioinfo.cms.uiws.system.dto.PageResponse; +import com.zioinfo.cms.uiws.system.dto.RoleDto; +import com.zioinfo.cms.uiws.system.dto.RoleSaveDto; +import com.zioinfo.cms.uiws.system.mapper.DeptRoleMapper; +import com.zioinfo.cms.uiws.system.mapper.RoleMapper; +import com.zioinfo.cms.uiws.system.mapper.RoleMenuMapper; +import com.zioinfo.cms.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 list(String keyword, int page, int size) { + List 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 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; + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/service/SysActor.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/SysActor.java new file mode 100644 index 0000000..54d6944 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/SysActor.java @@ -0,0 +1,24 @@ +package com.zioinfo.cms.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; + } +} diff --git a/backend/src/main/java/com/zioinfo/cms/uiws/system/service/UserService.java b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/UserService.java new file mode 100644 index 0000000..7be9a35 --- /dev/null +++ b/backend/src/main/java/com/zioinfo/cms/uiws/system/service/UserService.java @@ -0,0 +1,234 @@ +package com.zioinfo.cms.uiws.system.service; + +import com.zioinfo.cms.uiws.common.UiwsApiException; +import com.zioinfo.cms.uiws.common.UiwsErrorCode; +import com.zioinfo.cms.uiws.common.mail.MailSender; +import com.zioinfo.cms.uiws.system.dto.CheckIdResponse; +import com.zioinfo.cms.uiws.system.dto.PageResponse; +import com.zioinfo.cms.uiws.system.dto.UserDto; +import com.zioinfo.cms.uiws.system.dto.UserSaveDto; +import com.zioinfo.cms.uiws.system.mapper.CompanyMapper; +import com.zioinfo.cms.uiws.system.mapper.DeptMapper; +import com.zioinfo.cms.uiws.system.mapper.SysUserMapper; +import com.zioinfo.cms.uiws.system.model.SysCompany; +import com.zioinfo.cms.uiws.system.model.SysDept; +import com.zioinfo.cms.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 list(String keyword, String deptId, int page, int size) { + Map deptNames = deptNameMap(); + Map companyNames = companyNameMap(); + List 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 searchPopup(String keyword, String deptId) { + Map deptNames = deptNameMap(); + Map 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 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(), "[GUARDiA CMS] 비밀번호 초기화", + 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 deptNames, Map 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 deptNameMap() { + return deptMapper.findAll().stream() + .collect(Collectors.toMap(SysDept::getDeptId, SysDept::getDeptNm, (a, b) -> a, HashMap::new)); + } + + private Map 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(); + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 5a0ec5c..a408d6c 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -15,7 +15,8 @@ spring: # 기존 schema.sql 은 deploy_server 가 psql 로 별도 적용(비멱등 cms_setting/cms_i18n 시드 충돌 회피). # 91_uiws_port.sql 단독은 완전 멱등 → mode:always 안전. continue-on-error 로 부분 실패 격리. mode: ${SQL_INIT_MODE:always} - schema-locations: classpath:db/91_uiws_port.sql + # 91=업무모듈+2FA ALTER, 92=system(시스템관리·권한관리) 이식. 둘 다 완전 멱등. + schema-locations: classpath:db/91_uiws_port.sql,classpath:db/92_uiws_system.sql continue-on-error: true servlet: multipart: diff --git a/backend/src/main/resources/db/91_uiws_port.sql b/backend/src/main/resources/db/91_uiws_port.sql index b1ff7cb..268d0cc 100644 --- a/backend/src/main/resources/db/91_uiws_port.sql +++ b/backend/src/main/resources/db/91_uiws_port.sql @@ -241,6 +241,11 @@ ALTER TABLE cms_user ADD COLUMN IF NOT EXISTS login_fail_count INT DEFAULT 0; ALTER TABLE cms_user ADD COLUMN IF NOT EXISTS locked BOOLEAN DEFAULT false; ALTER TABLE cms_user ADD COLUMN IF NOT EXISTS otp_secret VARCHAR(255); +-- [로그인 보조 이식] 회원가입 승인 게이트. 기존(시드/현존) 운영자 계정은 승인됨(true)으로 백필. +-- 신규 가입자는 approved=false 로 INSERT(승인 대기) → SUPERADMIN 활성화 전 로그인 차단. +ALTER TABLE cms_user ADD COLUMN IF NOT EXISTS approved BOOLEAN DEFAULT true; +UPDATE cms_user SET approved = true WHERE approved IS NULL; + -- 2FA 메일 발송 대상 시드(미설정 admin 계정에 기본 이메일 — 운영 배포 시 변경). -- email 미설정 시 LogMailSender 폴백(코드는 로그에만)으로 동작하므로 흐름은 보장됨. UPDATE cms_user SET email = 'admin@zioinfo.co.kr' WHERE username = 'admin' AND (email IS NULL OR email = ''); diff --git a/backend/src/main/resources/db/92_uiws_system.sql b/backend/src/main/resources/db/92_uiws_system.sql new file mode 100644 index 0000000..ec65f6d --- /dev/null +++ b/backend/src/main/resources/db/92_uiws_system.sql @@ -0,0 +1,229 @@ +-- ============================================================================ +-- UIWS system(시스템관리·권한관리) 이식 (CMS) — com.zioinfo.cms.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_ 프리픽스. CMS cms_user(BIGSERIAL)/cms_member 와 +-- 별개 계정 모델(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; + +-- end 92_uiws_system.sql diff --git a/backend/src/main/resources/mapper/UserMapper.xml b/backend/src/main/resources/mapper/UserMapper.xml index d603e9d..15f35b2 100644 --- a/backend/src/main/resources/mapper/UserMapper.xml +++ b/backend/src/main/resources/mapper/UserMapper.xml @@ -18,11 +18,13 @@ + + @@ -60,4 +62,46 @@ UPDATE cms_user SET locked = false, login_fail_count = 0 WHERE username = #{username} + + + + + + + + + INSERT INTO cms_user (username, password_hash, display_name, role, email, + is_active, approved, login_fail_count, locked) + VALUES (#{username}, #{passwordHash}, #{displayName}, 'VIEWER', #{email}, + true, false, 0, false) + + + + + + + + + UPDATE cms_user + SET password_hash = #{passwordHash}, locked = false, login_fail_count = 0 + WHERE username = #{username} + + diff --git a/backend/src/main/resources/mapper/uiws/system/CodeMapper.xml b/backend/src/main/resources/mapper/uiws/system/CodeMapper.xml new file mode 100644 index 0000000..32a7beb --- /dev/null +++ b/backend/src/main/resources/mapper/uiws/system/CodeMapper.xml @@ -0,0 +1,63 @@ + + + + + + + + AND (grp_cd ILIKE '%' || #{keyword} || '%' OR grp_nm ILIKE '%' || #{keyword} || '%') + + + + + + + + + + + + + INSERT INTO tb_uiws_code_grp (grp_cd, grp_nm, use_yn, created_by, created_at) + VALUES (#{grpCd}, #{grpNm}, #{useYn}, #{createdBy}, now()) + + + + UPDATE tb_uiws_code_grp SET + grp_nm = #{grpNm}, use_yn = #{useYn}, updated_by = #{updatedBy}, updated_at = now() + WHERE grp_cd = #{grpCd} + + + + DELETE FROM tb_uiws_code_grp WHERE grp_cd = #{grpCd} + + + + + + + + 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()) + + + + DELETE FROM tb_uiws_code WHERE grp_cd = #{grpCd} + + diff --git a/backend/src/main/resources/mapper/uiws/system/CompanyMapper.xml b/backend/src/main/resources/mapper/uiws/system/CompanyMapper.xml new file mode 100644 index 0000000..a38bc8c --- /dev/null +++ b/backend/src/main/resources/mapper/uiws/system/CompanyMapper.xml @@ -0,0 +1,59 @@ + + + + + + + + AND (company_id ILIKE '%' || #{keyword} || '%' OR company_nm ILIKE '%' || #{keyword} || '%') + + + + + + + + + + + + + + + + + + + INSERT INTO tb_uiws_company (company_id, company_nm, biz_no, use_yn, created_by, created_at) + VALUES (#{companyId}, #{companyNm}, #{bizNo}, #{useYn}, #{createdBy}, now()) + + + + UPDATE tb_uiws_company SET + company_nm = #{companyNm}, biz_no = #{bizNo}, use_yn = #{useYn}, + updated_by = #{updatedBy}, updated_at = now() + WHERE company_id = #{companyId} + + + + DELETE FROM tb_uiws_company WHERE company_id = #{companyId} + + diff --git a/backend/src/main/resources/mapper/uiws/system/DeptMapper.xml b/backend/src/main/resources/mapper/uiws/system/DeptMapper.xml new file mode 100644 index 0000000..faebd2c --- /dev/null +++ b/backend/src/main/resources/mapper/uiws/system/DeptMapper.xml @@ -0,0 +1,63 @@ + + + + + + + + AND (dept_id ILIKE '%' || #{keyword} || '%' OR dept_nm ILIKE '%' || #{keyword} || '%') + + + + + + + + + + + + + + + + + + + + + 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()) + + + + 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} + + + + DELETE FROM tb_uiws_dept WHERE dept_id = #{deptId} + + diff --git a/backend/src/main/resources/mapper/uiws/system/DeptRoleMapper.xml b/backend/src/main/resources/mapper/uiws/system/DeptRoleMapper.xml new file mode 100644 index 0000000..be213d4 --- /dev/null +++ b/backend/src/main/resources/mapper/uiws/system/DeptRoleMapper.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + 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 + + + + DELETE FROM tb_uiws_dept_role WHERE dept_id = #{deptId} AND role_id = #{roleId} + + diff --git a/backend/src/main/resources/mapper/uiws/system/MenuMapper.xml b/backend/src/main/resources/mapper/uiws/system/MenuMapper.xml new file mode 100644 index 0000000..2aacc54 --- /dev/null +++ b/backend/src/main/resources/mapper/uiws/system/MenuMapper.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + 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()) + + + + 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} + + + + DELETE FROM tb_uiws_menu WHERE menu_id = #{menuId} + + diff --git a/backend/src/main/resources/mapper/uiws/system/ProgramMapper.xml b/backend/src/main/resources/mapper/uiws/system/ProgramMapper.xml new file mode 100644 index 0000000..2dfe517 --- /dev/null +++ b/backend/src/main/resources/mapper/uiws/system/ProgramMapper.xml @@ -0,0 +1,59 @@ + + + + + + + WHERE 1=1 + + AND (program_id ILIKE '%' || #{keyword} || '%' OR program_nm ILIKE '%' || #{keyword} || '%') + + AND program_type = #{programType} + + + + + + + + + + + + + + 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()) + + + + 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} + + + + DELETE FROM tb_uiws_program WHERE program_id = #{programId} + + diff --git a/backend/src/main/resources/mapper/uiws/system/RoleMapper.xml b/backend/src/main/resources/mapper/uiws/system/RoleMapper.xml new file mode 100644 index 0000000..81fa31a --- /dev/null +++ b/backend/src/main/resources/mapper/uiws/system/RoleMapper.xml @@ -0,0 +1,46 @@ + + + + + + + + AND (role_id ILIKE '%' || #{keyword} || '%' OR role_nm ILIKE '%' || #{keyword} || '%') + + + + + + + + + + + + + INSERT INTO tb_uiws_role (role_id, role_nm, role_desc, use_yn, created_by, created_at) + VALUES (#{roleId}, #{roleNm}, #{roleDesc}, #{useYn}, #{createdBy}, now()) + + + + UPDATE tb_uiws_role SET + role_nm = #{roleNm}, role_desc = #{roleDesc}, use_yn = #{useYn}, + updated_by = #{updatedBy}, updated_at = now() + WHERE role_id = #{roleId} + + + + DELETE FROM tb_uiws_role WHERE role_id = #{roleId} + + diff --git a/backend/src/main/resources/mapper/uiws/system/RoleMenuMapper.xml b/backend/src/main/resources/mapper/uiws/system/RoleMenuMapper.xml new file mode 100644 index 0000000..c82a702 --- /dev/null +++ b/backend/src/main/resources/mapper/uiws/system/RoleMenuMapper.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + 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 + + + + DELETE FROM tb_uiws_role_menu WHERE role_id = #{roleId} + + diff --git a/backend/src/main/resources/mapper/uiws/system/SysUserMapper.xml b/backend/src/main/resources/mapper/uiws/system/SysUserMapper.xml new file mode 100644 index 0000000..980b449 --- /dev/null +++ b/backend/src/main/resources/mapper/uiws/system/SysUserMapper.xml @@ -0,0 +1,70 @@ + + + + + + + WHERE 1=1 + + AND (user_id ILIKE '%' || #{keyword} || '%' + OR user_nm ILIKE '%' || #{keyword} || '%' + OR email ILIKE '%' || #{keyword} || '%') + + AND dept_id = #{deptId} + + + + + + + + + + + + + + + + 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()) + + + + 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}, + password = #{password}, + updated_by = #{updatedBy}, updated_at = now() + WHERE user_id = #{userId} + + diff --git a/frontend/src/api/uiws.ts b/frontend/src/api/uiws.ts index 9c780ff..88dae9c 100644 --- a/frontend/src/api/uiws.ts +++ b/frontend/src/api/uiws.ts @@ -11,6 +11,15 @@ import api from './client' export const verify2fa = (verifyToken: string, code: string) => api.post('/api/cms/auth/verify', { verifyToken, code }) +// ── 로그인 보조 3종(UIWS auth 패턴 이식, prefix /api/auth — permitAll) +// 대상=운영자 계정(cms_user). 회원가입=승인대기, 아이디찾기=마스킹, 비번초기화=메일/로그(응답 무노출). +export const signup = (body: { username: string; password: string; displayName?: string; email: string }) => + api.post('/api/auth/signup', body) +export const findId = (body: { displayName: string; email: string }) => + api.post('/api/auth/find-id', body) +export const resetPasswordRequest = (body: { username: string; email: string }) => + api.post('/api/auth/reset-password', body) + // ── 쪽지(message) export const sendMessage = (body: object) => api.post('/api/messages', body) export const listSent = (params: Record) => api.get('/api/messages/sent', { params }) diff --git a/frontend/src/pages/AuthHelperModals.tsx b/frontend/src/pages/AuthHelperModals.tsx new file mode 100644 index 0000000..e89944c --- /dev/null +++ b/frontend/src/pages/AuthHelperModals.tsx @@ -0,0 +1,191 @@ +import { useState } from 'react' +import { signup, findId, resetPasswordRequest } from '../api/uiws' + +/** + * 로그인 보조기능 3종 모달(UIWS auth 패턴 이식, CMS 디자인토큰). + * - 회원가입(signup): 승인 대기 안내. - 아이디찾기(find-id): 마스킹 username 표시. + * - 비밀번호 초기화(reset): 임시비번 메일/로그 발송 안내(코드/비번 화면 무노출). + * 디자인토큰: bg-ink/bg-panel/bg-card/border-edge/bg-brand/text-ink — Login.tsx 와 동일(다크 테마 정합). + * 색상 하드코딩 없이 테마 토큰만 사용 → 모듈 스코프 격리, 전역 테마 무영향. + */ + +export type AuthHelperKind = 'signup' | 'findId' | 'reset' + +const field = + 'w-full mb-3 px-3 py-2 rounded-lg bg-card border border-edge text-sm focus:border-brand outline-none' +const labelCls = 'block text-xs text-slate-400 mb-1' + +export default function AuthHelperModals({ + kind, + onClose, +}: { + kind: AuthHelperKind | null + onClose: () => void +}) { + if (!kind) return null + return ( +
+
e.stopPropagation()} + > + {kind === 'signup' && } + {kind === 'findId' && } + {kind === 'reset' && } +
+
+ ) +} + +function Header({ title }: { title: string }) { + return

{title}

+} + +function CloseBtn({ onClose }: { onClose: () => void }) { + return ( + + ) +} + +function SignupForm({ onClose }: { onClose: () => void }) { + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [displayName, setDisplayName] = useState('') + const [email, setEmail] = useState('') + const [msg, setMsg] = useState('') + const [ok, setOk] = useState(false) + const [busy, setBusy] = useState(false) + + const submit = async (e: React.FormEvent) => { + e.preventDefault() + setMsg(''); setBusy(true) + try { + const res = await signup({ username, password, displayName, email }) + const d = res.data?.data || {} + setOk(!!d.success) + setMsg(d.message || (d.success ? '가입 신청이 접수되었습니다.' : '가입 신청에 실패했습니다.')) + } catch { + setOk(false) + setMsg('가입 신청에 실패했습니다. 입력값을 확인하세요.') + } finally { + setBusy(false) + } + } + + return ( +
+
+

가입 후 관리자 승인을 거쳐야 로그인할 수 있습니다.

+ + setUsername(e.target.value)} className={field} required /> + + setPassword(e.target.value)} className={field} required /> + + setDisplayName(e.target.value)} className={field} /> + + setEmail(e.target.value)} className={field} required /> + {msg &&

{msg}

} + {!ok && ( + + )} + + + ) +} + +function FindIdForm({ onClose }: { onClose: () => void }) { + const [displayName, setDisplayName] = useState('') + const [email, setEmail] = useState('') + const [result, setResult] = useState(null) + const [err, setErr] = useState('') + const [busy, setBusy] = useState(false) + + const submit = async (e: React.FormEvent) => { + e.preventDefault() + setErr(''); setResult(null); setBusy(true) + try { + const res = await findId({ displayName, email }) + const d = res.data?.data || {} + if (d.found) setResult(d.maskedUsername || '') + else setErr('일치하는 계정을 찾지 못했습니다. 이름·이메일을 확인하세요.') + } catch { + setErr('조회에 실패했습니다.') + } finally { + setBusy(false) + } + } + + return ( +
+
+

가입 시 등록한 이름과 이메일로 아이디를 찾습니다.

+ + setDisplayName(e.target.value)} className={field} required /> + + setEmail(e.target.value)} className={field} required /> + {result !== null && ( +

+ 회원님의 아이디: {result} +

+ )} + {err &&

{err}

} + + + + ) +} + +function ResetForm({ onClose }: { onClose: () => void }) { + const [username, setUsername] = useState('') + const [email, setEmail] = useState('') + const [msg, setMsg] = useState('') + const [ok, setOk] = useState(false) + const [busy, setBusy] = useState(false) + + const submit = async (e: React.FormEvent) => { + e.preventDefault() + setMsg(''); setBusy(true) + try { + const res = await resetPasswordRequest({ username, email }) + const d = res.data?.data || {} + setOk(!!d.success) + setMsg(d.message || '임시 비밀번호를 이메일로 발송했습니다.') + } catch { + setOk(false) + setMsg('요청에 실패했습니다. 입력값을 확인하세요.') + } finally { + setBusy(false) + } + } + + return ( +
+
+

아이디와 이메일을 확인 후 임시 비밀번호를 메일로 보내드립니다.

+ + setUsername(e.target.value)} className={field} required /> + + setEmail(e.target.value)} className={field} required /> + {msg &&

{msg}

} + {!ok && ( + + )} + + + ) +} diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 522554c..92e0d21 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { useNavigate } from 'react-router-dom' import { login, getMe } from '../api/client' import { verify2fa } from '../api/uiws' +import AuthHelperModals, { type AuthHelperKind } from './AuthHelperModals' /** * CMS 로그인 — UIWS 2FA 레이어 대응. @@ -19,6 +20,7 @@ export default function Login() { const [maskedEmail, setMaskedEmail] = useState('') const [err, setErr] = useState('') const [busy, setBusy] = useState(false) + const [helper, setHelper] = useState(null) const nav = useNavigate() // 토큰 저장 + 역할 선조회 후 대시보드 진입(공통) @@ -96,6 +98,14 @@ export default function Login() { {busy ? '로그인 중…' : '로그인'}

admin / editor / author · admin123

+ {/* 로그인 보조 3링크(UIWS auth 패턴 이식) */} +
+ + | + + | + +
) : ( <> @@ -117,6 +127,7 @@ export default function Login() { )} + setHelper(null)} /> ) }