feat: UIMS 로그인보조+권한관리(RBAC) 이식
This commit is contained in:
parent
8e0481e840
commit
337c8beffa
@ -1,22 +1,35 @@
|
|||||||
package com.zioinfo.esn.auth;
|
package com.zioinfo.esn.auth;
|
||||||
|
|
||||||
import com.zioinfo.esn.common.ApiResponse;
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.auth.TwoFactorService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ESN 인증 컨트롤러.
|
||||||
|
* - /login: 2FA off 면 { twofa:"false", token, type }, 2FA on 이면 { twofa:"true", verifyToken, step, maskedEmail }.
|
||||||
|
* - /verify: (UIWS 2FA 이식) verify-token + 인증코드 → access 토큰 발급.
|
||||||
|
* 기존 클라이언트는 응답에 token 필드가 그대로 존재(2FA off 시) → 회귀 0.
|
||||||
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/auth")
|
@RequestMapping("/api/auth")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AuthController {
|
public class AuthController {
|
||||||
|
|
||||||
private final AuthService authService;
|
private final AuthService authService;
|
||||||
|
private final TwoFactorService twoFactorService;
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public ApiResponse<Map<String, String>> login(@RequestBody LoginRequest req) {
|
public ApiResponse<Map<String, String>> login(@RequestBody LoginRequest req) {
|
||||||
String token = authService.login(req.username(), req.password());
|
return ApiResponse.ok(authService.login(req.username(), req.password()));
|
||||||
return ApiResponse.ok(Map.of("token", token, "type", "Bearer"));
|
}
|
||||||
|
|
||||||
|
/** UIWS 2FA 이식: 2차 인증 코드 검증 → access 토큰 발급. */
|
||||||
|
@PostMapping("/verify")
|
||||||
|
public ApiResponse<Map<String, String>> verify(@RequestBody VerifyRequest req) {
|
||||||
|
return ApiResponse.ok(twoFactorService.verify(req.verifyToken(), req.code()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/me")
|
@GetMapping("/me")
|
||||||
@ -32,4 +45,6 @@ public class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
record LoginRequest(String username, String password) {}
|
record LoginRequest(String username, String password) {}
|
||||||
|
|
||||||
|
record VerifyRequest(String verifyToken, String code) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,52 @@
|
|||||||
|
package com.zioinfo.esn.auth;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.auth.dto.FindIdRequest;
|
||||||
|
import com.zioinfo.esn.auth.dto.FindIdResponse;
|
||||||
|
import com.zioinfo.esn.auth.dto.ResetPwRequest;
|
||||||
|
import com.zioinfo.esn.auth.dto.SignupRequest;
|
||||||
|
import com.zioinfo.esn.auth.dto.SignupResponse;
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인 보조 기능 3종(UIWS auth 패턴 이식) — 회원가입 / 아이디찾기 / 비밀번호 초기화.
|
||||||
|
*
|
||||||
|
* <p>base path {@code /api/auth} (SecurityConfig permitAll — 로그인 전 무인증 접근).
|
||||||
|
* 대상은 ESN 사용자 계정(esn_user).
|
||||||
|
* <ul>
|
||||||
|
* <li>회원가입: 승인 대기(approved=false) 상태로 등록 → ADMIN 승인 전 로그인 차단</li>
|
||||||
|
* <li>아이디찾기: displayName+email 매칭, username 부분 마스킹 반환</li>
|
||||||
|
* <li>비번초기화: username+email 검증 → 임시비번 BCrypt 저장 + 메일/로그 발송(응답에 비번 미포함)</li>
|
||||||
|
* </ul>
|
||||||
|
* 보안 불변규칙: 임시비번·자격증명·계정 존재여부를 응답/로그 메시지에 노출하지 않는다.
|
||||||
|
* (기존 2FA 로그인/검증 컨트롤러 AuthController(/api/auth/login,/verify)와 별개 — 회귀 0.)
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/auth")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AuthHelperController {
|
||||||
|
|
||||||
|
private final AuthHelperService authHelperService;
|
||||||
|
|
||||||
|
/** 회원가입(승인 대기 INSERT). */
|
||||||
|
@PostMapping("/signup")
|
||||||
|
public ApiResponse<SignupResponse> signup(@RequestBody SignupRequest req) {
|
||||||
|
return ApiResponse.ok(authHelperService.signup(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 아이디 찾기(이메일+이름 매칭, 마스킹 반환). */
|
||||||
|
@PostMapping("/find-id")
|
||||||
|
public ApiResponse<FindIdResponse> findId(@RequestBody FindIdRequest req) {
|
||||||
|
return ApiResponse.ok(authHelperService.findId(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 비밀번호 초기화(검증 → 임시비번 BCrypt + 메일/로그). */
|
||||||
|
@PostMapping("/reset-password")
|
||||||
|
public ApiResponse<SignupResponse> resetPassword(@RequestBody ResetPwRequest req) {
|
||||||
|
return ApiResponse.ok(authHelperService.resetPassword(req));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,132 @@
|
|||||||
|
package com.zioinfo.esn.auth;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.auth.dto.FindIdRequest;
|
||||||
|
import com.zioinfo.esn.auth.dto.FindIdResponse;
|
||||||
|
import com.zioinfo.esn.auth.dto.ResetPwRequest;
|
||||||
|
import com.zioinfo.esn.auth.dto.SignupRequest;
|
||||||
|
import com.zioinfo.esn.auth.dto.SignupResponse;
|
||||||
|
import com.zioinfo.esn.auth.mapper.UserSignupMapper;
|
||||||
|
import com.zioinfo.esn.uiws.common.mail.MailSender;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인 보조 3종(UIWS auth 패턴 이식) — 회원가입 / 아이디찾기 / 비밀번호 초기화.
|
||||||
|
* 기존 2FA 로그인 서비스(AuthService)와 별개 — 회귀 0.
|
||||||
|
*
|
||||||
|
* <p>보안 불변규칙: 임시비번·자격증명·계정 존재여부를 응답/로그 메시지에 노출하지 않는다.
|
||||||
|
* 메일 발송은 MailSender(미설정 시 LogMailSender 폴백)만 사용 — 외부 API 호출 없음.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AuthHelperService {
|
||||||
|
|
||||||
|
private final UserSignupMapper userSignupMapper;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final MailSender mailSender;
|
||||||
|
|
||||||
|
private static final SecureRandom RANDOM = new SecureRandom();
|
||||||
|
// 혼동 문자(0/O/1/l/I) 제외 — 임시비번 가독성.
|
||||||
|
private static final String TMP_PW_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회원가입(승인 대기 INSERT). username/email 중복 차단, 비번 BCrypt, approved=false.
|
||||||
|
* 보안: 응답에 자격증명·임시정보 미포함, 일반 메시지만.
|
||||||
|
*/
|
||||||
|
public SignupResponse 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 SignupResponse(false, "아이디·비밀번호(4자 이상)·이메일은 필수입니다.");
|
||||||
|
}
|
||||||
|
if (userSignupMapper.countByUsername(req.username()) > 0) {
|
||||||
|
return new SignupResponse(false, "이미 사용 중인 아이디입니다.");
|
||||||
|
}
|
||||||
|
if (userSignupMapper.countByEmail(req.email()) > 0) {
|
||||||
|
return new SignupResponse(false, "이미 등록된 이메일입니다.");
|
||||||
|
}
|
||||||
|
EsnUser u = new EsnUser();
|
||||||
|
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());
|
||||||
|
// insertSignup: role='USER', approved=false, locked=false, login_fail_count=0 (XML 고정)
|
||||||
|
userSignupMapper.insertSignup(u);
|
||||||
|
log.info("[auth-helper] signup pending approval: username={}", req.username());
|
||||||
|
return new SignupResponse(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, "");
|
||||||
|
}
|
||||||
|
EsnUser u = userSignupMapper.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 SignupResponse resetPassword(ResetPwRequest req) {
|
||||||
|
final String okMsg = "임시 비밀번호를 등록된 이메일로 발송했습니다. 메일을 확인하세요.";
|
||||||
|
if (req.username() == null || req.username().isBlank()
|
||||||
|
|| req.email() == null || req.email().isBlank()) {
|
||||||
|
return new SignupResponse(false, "아이디와 이메일을 모두 입력하세요.");
|
||||||
|
}
|
||||||
|
EsnUser u = userSignupMapper.findByUsernameAndEmail(req.username(), req.email());
|
||||||
|
if (u == null) {
|
||||||
|
// 존재 여부 누설 방지 — 동일 성공 메시지 반환(실제 발송 없음).
|
||||||
|
log.info("[auth-helper] reset-password no match (suppressed): username={}", req.username());
|
||||||
|
return new SignupResponse(true, okMsg);
|
||||||
|
}
|
||||||
|
String tempPw = generateTempPassword();
|
||||||
|
// applyTempPassword: BCrypt 저장 + pw_change_yn=true + locked=false + login_fail_count=0 (멱등)
|
||||||
|
userSignupMapper.applyTempPassword(req.username(), passwordEncoder.encode(tempPw));
|
||||||
|
|
||||||
|
String subject = "[zioinfo-esn] 임시 비밀번호 안내";
|
||||||
|
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 SignupResponse(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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,21 @@
|
|||||||
package com.zioinfo.esn.auth;
|
package com.zioinfo.esn.auth;
|
||||||
|
|
||||||
import com.zioinfo.esn.auth.mapper.UserAuthMapper;
|
import com.zioinfo.esn.auth.mapper.UserAuthMapper;
|
||||||
|
import com.zioinfo.esn.uiws.auth.TwoFactorService;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsApiException;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsErrorCode;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ESN 인증 서비스.
|
||||||
|
* - 기존 단일 JWT 로그인 보존(2FA off 또는 미설정 시 회귀 0).
|
||||||
|
* - UIWS 2FA 레이어(이메일 코드) 추가: 2FA on 이면 1차 통과 후 verify-token + 이메일코드 발급.
|
||||||
|
* 실패 누적 max-login-fail 회 시 계정 잠금.
|
||||||
|
*/
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AuthService {
|
public class AuthService {
|
||||||
@ -14,17 +23,57 @@ public class AuthService {
|
|||||||
private final UserAuthMapper userMapper;
|
private final UserAuthMapper userMapper;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
private final JwtUtil jwtUtil;
|
private final JwtUtil jwtUtil;
|
||||||
|
private final TwoFactorService twoFactorService;
|
||||||
|
|
||||||
public String login(String username, String password) {
|
/**
|
||||||
|
* 1차 로그인. 2FA 활성 시 verify-token + 이메일코드 흐름으로 분기,
|
||||||
|
* 비활성 시 기존처럼 access 토큰 즉시 발급.
|
||||||
|
*
|
||||||
|
* @return 2FA off: { token, type, twofa:"false" }
|
||||||
|
* 2FA on : { verifyToken, step:"EMAIL", maskedEmail, twofa:"true" }
|
||||||
|
*/
|
||||||
|
public Map<String, String> login(String username, String password) {
|
||||||
EsnUser user = userMapper.findByUsername(username);
|
EsnUser user = userMapper.findByUsername(username);
|
||||||
|
|
||||||
|
// 잠금 우선 차단(존재하는 계정에 한해 잠금 메시지 — 존재 여부 누설 최소화)
|
||||||
|
if (user != null && twoFactorService.isLocked(user)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.ACCOUNT_LOCKED);
|
||||||
|
}
|
||||||
if (user == null || !user.isActive()) {
|
if (user == null || !user.isActive()) {
|
||||||
throw new RuntimeException("ERR-AUTH-001: 존재하지 않거나 비활성 계정");
|
throw new RuntimeException("ERR-AUTH-001: 존재하지 않거나 비활성 계정");
|
||||||
}
|
}
|
||||||
|
// 회원가입 승인 게이트(UIWS 로그인 보조 이식): 가입 신청자(approved=false)는 관리자 승인 전 로그인 차단.
|
||||||
|
// 기존 계정은 approved=true(90_uiws_system.sql 멱등 보정)라 회귀 없음.
|
||||||
|
if (Boolean.FALSE.equals(user.getApproved())) {
|
||||||
|
throw new RuntimeException("ERR-AUTH-003: 승인 대기 중인 계정입니다. 관리자 승인 후 로그인하세요.");
|
||||||
|
}
|
||||||
if (!passwordEncoder.matches(password, user.getPasswordHash())) {
|
if (!passwordEncoder.matches(password, user.getPasswordHash())) {
|
||||||
|
// 2FA 활성 시 실패 누적/잠금. 비활성 시 기존 동작(메시지만) 유지.
|
||||||
|
if (twoFactorService.isEnabled()) {
|
||||||
|
twoFactorService.recordLoginFailure(username);
|
||||||
|
EsnUser after = userMapper.findByUsername(username);
|
||||||
|
if (after != null && Boolean.TRUE.equals(after.getLocked())) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.ACCOUNT_LOCKED);
|
||||||
|
}
|
||||||
|
}
|
||||||
throw new RuntimeException("ERR-AUTH-002: 비밀번호 불일치");
|
throw new RuntimeException("ERR-AUTH-002: 비밀번호 불일치");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 비밀번호 검증 통과
|
||||||
|
if (twoFactorService.isEnabled()) {
|
||||||
|
Map<String, String> step1 = twoFactorService.beginTwoFactor(user);
|
||||||
|
return Map.of(
|
||||||
|
"twofa", "true",
|
||||||
|
"verifyToken", step1.get("verifyToken"),
|
||||||
|
"step", step1.get("step"),
|
||||||
|
"maskedEmail", step1.getOrDefault("maskedEmail", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2FA 비활성 — 기존 단일 로그인 흐름(회귀 0)
|
||||||
|
userMapper.resetLoginFail(username);
|
||||||
userMapper.updateLastLogin(username);
|
userMapper.updateLastLogin(username);
|
||||||
return jwtUtil.generate(username, user.getRole(), user.getTenantCode());
|
String token = jwtUtil.generate(username, user.getRole(), user.getTenantCode());
|
||||||
|
return Map.of("twofa", "false", "token", token, "type", "Bearer");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, String> me(String token) {
|
public Map<String, String> me(String token) {
|
||||||
|
|||||||
@ -18,4 +18,27 @@ public class EsnUser {
|
|||||||
private boolean active;
|
private boolean active;
|
||||||
private LocalDateTime lastLoginAt;
|
private LocalDateTime lastLoginAt;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
// ── UIWS 2FA 이식 컬럼 (esn_user ALTER, db/91_uiws_port.sql) ─────────────────
|
||||||
|
/** 이메일 인증코드(6자리). 발급 후 verify 단계에서 검증. API 응답에는 절대 미포함. */
|
||||||
|
@JsonIgnore
|
||||||
|
private String emailVerifyCode;
|
||||||
|
/** 인증코드 만료시각. */
|
||||||
|
@JsonIgnore
|
||||||
|
private LocalDateTime emailVerifyExpire;
|
||||||
|
/** 로그인 실패 누적 횟수(기본 0). max-login-fail 도달 시 locked. */
|
||||||
|
private Integer loginFailCount;
|
||||||
|
/** 계정 잠금 여부(기본 false). */
|
||||||
|
private Boolean locked;
|
||||||
|
/** TOTP 시크릿(UIWS OTP 경로 대비, 현재 이메일 흐름에서는 미사용). */
|
||||||
|
@JsonIgnore
|
||||||
|
private String otpSecret;
|
||||||
|
|
||||||
|
// ── UIWS 로그인 보조 이식 컬럼 (esn_user ALTER, db/90_uiws_system.sql) ────────
|
||||||
|
/** 회원가입 승인 여부(기본 true=기존계정). 신규 가입자는 false → ADMIN 승인 전 로그인 차단. */
|
||||||
|
private Boolean approved;
|
||||||
|
/** 표시명(아이디찾기 매칭용). */
|
||||||
|
private String displayName;
|
||||||
|
/** 임시비번 발급 후 비밀번호 변경 유도 플래그. */
|
||||||
|
private Boolean pwChangeYn;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,10 @@ public class JwtFilter extends OncePerRequestFilter {
|
|||||||
String header = req.getHeader("Authorization");
|
String header = req.getHeader("Authorization");
|
||||||
if (header != null && header.startsWith("Bearer ")) {
|
if (header != null && header.startsWith("Bearer ")) {
|
||||||
String token = header.substring(7);
|
String token = header.substring(7);
|
||||||
if (jwtUtil.isValid(token)) {
|
// 보안(★치명 — UIWS 2FA): purpose=2fa verify-token 은 access 토큰이 아니다.
|
||||||
|
// 동일 서명키라 isValid()는 통과하므로 별도 차단하지 않으면 2차 인증 전 보호 API 접근(2FA 완전 우회)이 가능.
|
||||||
|
// → verify-token 은 인증 컨텍스트를 세우지 않고 무시한다(/api/auth/verify 에서만 사용).
|
||||||
|
if (jwtUtil.isValid(token) && !jwtUtil.isVerifyToken(token)) {
|
||||||
String username = jwtUtil.getUsername(token);
|
String username = jwtUtil.getUsername(token);
|
||||||
String role = jwtUtil.getRole(token);
|
String role = jwtUtil.getRole(token);
|
||||||
var auth = new UsernamePasswordAuthenticationToken(
|
var auth = new UsernamePasswordAuthenticationToken(
|
||||||
|
|||||||
@ -35,6 +35,48 @@ public class JwtUtil {
|
|||||||
.compact();
|
.compact();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UIWS 2FA 이식: 1차 로그인 통과 후 발급하는 단기 verify-token.
|
||||||
|
* purpose=2fa 클레임으로 access 토큰과 구분(verify-token 으로는 보호 API 접근 불가 — JwtFilter 에서 차단).
|
||||||
|
*/
|
||||||
|
public String generateVerifyToken(String username, long validitySeconds) {
|
||||||
|
return Jwts.builder()
|
||||||
|
.subject(username)
|
||||||
|
.claim("purpose", "2fa")
|
||||||
|
.issuedAt(new Date())
|
||||||
|
.expiration(new Date(System.currentTimeMillis() + validitySeconds * 1000L))
|
||||||
|
.signWith(key())
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** verify-token 검증 후 username 반환. 유효하지 않거나 purpose!=2fa 면 null. */
|
||||||
|
public String parseVerifyTokenUsername(String token) {
|
||||||
|
try {
|
||||||
|
Claims c = parse(token);
|
||||||
|
if (!"2fa".equals(c.get("purpose", String.class))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return c.getSubject();
|
||||||
|
} catch (JwtException | IllegalArgumentException e) {
|
||||||
|
log.debug("verify-token 검증 실패: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 보안(★치명): 토큰이 2FA verify-token(purpose=2fa)인지 판별.
|
||||||
|
* verify-token 은 access 와 같은 서명키라 isValid()는 통과하므로, JwtFilter 가 별도로
|
||||||
|
* 걸러내지 않으면 2차 코드 검증 없이 보호 API 접근(2FA 완전 우회)이 된다.
|
||||||
|
* → JwtFilter 는 purpose=2fa 토큰을 인증 컨텍스트로 세우지 않는다(/api/auth/verify 에서만 사용).
|
||||||
|
*/
|
||||||
|
public boolean isVerifyToken(String token) {
|
||||||
|
try {
|
||||||
|
return "2fa".equals(parse(token).get("purpose", String.class));
|
||||||
|
} catch (JwtException | IllegalArgumentException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Claims parse(String token) {
|
public Claims parse(String token) {
|
||||||
return Jwts.parser().verifyWith(key()).build()
|
return Jwts.parser().verifyWith(key()).build()
|
||||||
.parseSignedClaims(token).getPayload();
|
.parseSignedClaims(token).getPayload();
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.zioinfo.esn.auth.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인 보조 이식 — 아이디 찾기 요청 DTO.
|
||||||
|
* 표시명(displayName) + 이메일(email) 동시 일치하는 ESN 계정을 조회.
|
||||||
|
* 응답의 username 은 마스킹하여 반환(자격증명 보호).
|
||||||
|
*/
|
||||||
|
public record FindIdRequest(
|
||||||
|
String displayName,
|
||||||
|
String email) {
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.zioinfo.esn.auth.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인 보조 이식 — 아이디 찾기 응답 DTO.
|
||||||
|
* found=true 이면 maskedUsername(예: ad***) 동봉. 미발견이어도 동일 shape(존재 여부 누설 최소화).
|
||||||
|
* 원본 username 전체는 절대 노출하지 않는다(부분 마스킹만).
|
||||||
|
*/
|
||||||
|
public record FindIdResponse(
|
||||||
|
boolean found,
|
||||||
|
String maskedUsername) {
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.zioinfo.esn.auth.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인 보조 이식 — 비밀번호 초기화 요청 DTO.
|
||||||
|
* username + email 동시 일치 검증 후 임시 비밀번호를 BCrypt 로 저장.
|
||||||
|
* 임시 비밀번호는 메일(미설정 시 LogMailSender 로그)로만 전달 — API 응답에 절대 미포함.
|
||||||
|
*/
|
||||||
|
public record ResetPwRequest(
|
||||||
|
String username,
|
||||||
|
String email) {
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.zioinfo.esn.auth.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인 보조 이식 — 회원가입 요청 DTO.
|
||||||
|
* 대상: ESN 사용자 계정(esn_user). 가입 결과는 승인 대기(approved=false) 상태로 INSERT →
|
||||||
|
* ADMIN 승인 전 로그인 차단(AuthService.login 의 approved 게이트).
|
||||||
|
*/
|
||||||
|
public record SignupRequest(
|
||||||
|
String username,
|
||||||
|
String password,
|
||||||
|
String displayName,
|
||||||
|
String email) {
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.zioinfo.esn.auth.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인 보조 이식 — 회원가입/비밀번호초기화 공통 결과 DTO.
|
||||||
|
* 항상 일반 메시지만 반환(임시비번·계정 존재여부 등 민감정보 미포함 — 자격증명 보호 불변규칙).
|
||||||
|
* 보안상 비밀번호 초기화는 대상 미존재 시에도 success=true(열거 공격 방지).
|
||||||
|
*/
|
||||||
|
public record SignupResponse(
|
||||||
|
boolean success,
|
||||||
|
String message) {
|
||||||
|
}
|
||||||
@ -4,8 +4,29 @@ import com.zioinfo.esn.auth.EsnUser;
|
|||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface UserAuthMapper {
|
public interface UserAuthMapper {
|
||||||
EsnUser findByUsername(@Param("username") String username);
|
EsnUser findByUsername(@Param("username") String username);
|
||||||
int updateLastLogin(@Param("username") String username);
|
int updateLastLogin(@Param("username") String username);
|
||||||
|
|
||||||
|
// ── UIWS 2FA 이식: 실패 카운트/잠금/인증코드 갱신 (멱등 UPDATE) ───────────────
|
||||||
|
|
||||||
|
/** 로그인 성공 시 실패 카운트 초기화. */
|
||||||
|
int resetLoginFail(@Param("username") String username);
|
||||||
|
|
||||||
|
/** 로그인 실패 누적(+1) 및 임계 도달 시 잠금. */
|
||||||
|
int incrementLoginFail(@Param("username") String username, @Param("maxFail") int maxFail);
|
||||||
|
|
||||||
|
/** 1차 통과 시 이메일 인증코드/만료 저장(verify 단계에서 검증). */
|
||||||
|
int saveEmailCode(@Param("username") String username,
|
||||||
|
@Param("code") String code,
|
||||||
|
@Param("expire") LocalDateTime expire);
|
||||||
|
|
||||||
|
/** 2차 검증 성공 시 코드 폐기. */
|
||||||
|
int clearEmailCode(@Param("username") String username);
|
||||||
|
|
||||||
|
/** 관리자 잠금 해제(실패 카운트/잠금 초기화). */
|
||||||
|
int unlock(@Param("username") String username);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
package com.zioinfo.esn.auth.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.auth.EsnUser;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인 보조 3종(회원가입·아이디찾기·비번초기화)용 esn_user 매퍼.
|
||||||
|
* 기존 2FA 로그인 매퍼(UserAuthMapper)와 분리 — 회귀 0.
|
||||||
|
* 보안: 임시비번/자격증명은 응답·로그(코드 외)로 노출하지 않는다(불변규칙).
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface UserSignupMapper {
|
||||||
|
|
||||||
|
/** 가입 중복 검사: username 존재 여부. */
|
||||||
|
int countByUsername(@Param("username") String username);
|
||||||
|
|
||||||
|
/** 가입 중복 검사: email 존재 여부. */
|
||||||
|
int countByEmail(@Param("email") String email);
|
||||||
|
|
||||||
|
/** 회원가입(승인 대기): role='USER', approved=false, is_active=true 고정 INSERT. */
|
||||||
|
int insertSignup(EsnUser user);
|
||||||
|
|
||||||
|
/** 아이디찾기: 표시명(display_name)+이메일 일치 사용자(존재 시 마스킹 반환). */
|
||||||
|
EsnUser findByDisplayNameAndEmail(@Param("displayName") String displayName,
|
||||||
|
@Param("email") String email);
|
||||||
|
|
||||||
|
/** 비번초기화: username+email 동시 일치 검증용. */
|
||||||
|
EsnUser findByUsernameAndEmail(@Param("username") String username,
|
||||||
|
@Param("email") String email);
|
||||||
|
|
||||||
|
/** 비번초기화: 임시비번 적용 + 변경유도 + 잠금/실패카운트 해제(멱등 UPDATE). */
|
||||||
|
int applyTempPassword(@Param("username") String username,
|
||||||
|
@Param("passwordHash") String passwordHash);
|
||||||
|
}
|
||||||
@ -51,7 +51,7 @@ public class OllamaClient {
|
|||||||
.uri(URI.create(baseUrl + "/api/chat"))
|
.uri(URI.create(baseUrl + "/api/chat"))
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(json))
|
.POST(HttpRequest.BodyPublishers.ofString(json))
|
||||||
.timeout(Duration.ofSeconds(30))
|
.timeout(Duration.ofSeconds(120))
|
||||||
.build();
|
.build();
|
||||||
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
if (resp.statusCode() == 200) {
|
if (resp.statusCode() == 200) {
|
||||||
|
|||||||
@ -36,6 +36,8 @@ public class SecurityConfig {
|
|||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
// 인증 불필요
|
// 인증 불필요
|
||||||
.requestMatchers("/api/auth/**").permitAll()
|
.requestMatchers("/api/auth/**").permitAll()
|
||||||
|
// UIWS 회원가입 화면 공개 조회(부서/거래처) — 비인증 접근.
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/public/**").permitAll()
|
||||||
.requestMatchers("/actuator/health").permitAll()
|
.requestMatchers("/actuator/health").permitAll()
|
||||||
// 정적 리소스 (React SPA)
|
// 정적 리소스 (React SPA)
|
||||||
.requestMatchers(HttpMethod.GET, "/", "/index.html", "/assets/**", "/*.js", "/*.css", "/*.ico").permitAll()
|
.requestMatchers(HttpMethod.GET, "/", "/index.html", "/assets/**", "/*.js", "/*.css", "/*.ico").permitAll()
|
||||||
@ -44,6 +46,10 @@ public class SecurityConfig {
|
|||||||
.requestMatchers("/api/tenants/**").hasRole("ADMIN")
|
.requestMatchers("/api/tenants/**").hasRole("ADMIN")
|
||||||
.requestMatchers("/api/users/**").hasAnyRole("ADMIN", "MANAGER")
|
.requestMatchers("/api/users/**").hasAnyRole("ADMIN", "MANAGER")
|
||||||
.requestMatchers("/api/firmware/**").hasAnyRole("ADMIN", "MANAGER")
|
.requestMatchers("/api/firmware/**").hasAnyRole("ADMIN", "MANAGER")
|
||||||
|
// UIWS system(권한관리): 조회 인증사용자, 변경(POST/PUT/DELETE)은 ADMIN/MANAGER.
|
||||||
|
.requestMatchers(HttpMethod.POST, "/api/system/**").hasAnyRole("ADMIN", "MANAGER")
|
||||||
|
.requestMatchers(HttpMethod.PUT, "/api/system/**").hasAnyRole("ADMIN", "MANAGER")
|
||||||
|
.requestMatchers(HttpMethod.DELETE, "/api/system/**").hasAnyRole("ADMIN", "MANAGER")
|
||||||
.requestMatchers(HttpMethod.DELETE, "/api/**").hasAnyRole("ADMIN", "MANAGER")
|
.requestMatchers(HttpMethod.DELETE, "/api/**").hasAnyRole("ADMIN", "MANAGER")
|
||||||
.requestMatchers(HttpMethod.POST, "/api/**").hasAnyRole("ADMIN", "MANAGER", "USER")
|
.requestMatchers(HttpMethod.POST, "/api/**").hasAnyRole("ADMIN", "MANAGER", "USER")
|
||||||
.requestMatchers(HttpMethod.PUT, "/api/**").hasAnyRole("ADMIN", "MANAGER", "USER")
|
.requestMatchers(HttpMethod.PUT, "/api/**").hasAnyRole("ADMIN", "MANAGER", "USER")
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
package com.zioinfo.esn.uiws.auth;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Update;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2FA 로그인 검증 이력 매퍼 (tb_uiws_login_verify). 발급/검증 감사 기록.
|
||||||
|
* verify_code 는 감사 이력에만 남고 API 응답에는 절대 노출하지 않는다.
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface LoginVerifyMapper {
|
||||||
|
|
||||||
|
int insert(UiwsLoginVerify v);
|
||||||
|
|
||||||
|
/** 동일 user 의 미검증 EMAIL 이력 중 최신 1건을 검증완료(Y) 처리. */
|
||||||
|
@Update("""
|
||||||
|
UPDATE tb_uiws_login_verify
|
||||||
|
SET verified_yn = 'Y', updated_by = #{userId}, updated_at = now()
|
||||||
|
WHERE verify_id = (
|
||||||
|
SELECT verify_id FROM tb_uiws_login_verify
|
||||||
|
WHERE user_id = #{userId} AND verify_method = 'EMAIL' AND verified_yn = 'N'
|
||||||
|
ORDER BY verify_id DESC LIMIT 1
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
int markLatestVerified(String userId);
|
||||||
|
}
|
||||||
@ -0,0 +1,161 @@
|
|||||||
|
package com.zioinfo.esn.uiws.auth;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.auth.EsnUser;
|
||||||
|
import com.zioinfo.esn.auth.JwtUtil;
|
||||||
|
import com.zioinfo.esn.auth.mapper.UserAuthMapper;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsApiException;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsErrorCode;
|
||||||
|
import com.zioinfo.esn.uiws.common.mail.MailSender;
|
||||||
|
import com.zioinfo.esn.uiws.config.UiwsProperties;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UIWS 2FA(이메일 코드) 레이어. ESN 기존 단일 로그인을 보존하면서 2단계 인증을 추가한다.
|
||||||
|
*
|
||||||
|
* 흐름:
|
||||||
|
* 1) 1차 로그인 성공 → {@link #beginTwoFactor}: verify-token 발급 + 이메일 인증코드 발송(LogMailSender 폴백) + 감사 기록.
|
||||||
|
* 2) {@code POST /api/auth/verify}(verifyToken + code) → {@link #verify}: 코드 검증 후 access 발급.
|
||||||
|
* 3) 로그인 실패 누적 max-login-fail 회 → 계정 잠금({@link #recordLoginFailure}).
|
||||||
|
*
|
||||||
|
* 보안:
|
||||||
|
* - 인증코드는 메일/감사 채널로만 전달. API 응답·로그 메시지에 코드/비밀번호/자격증명 절대 미노출(불변규칙).
|
||||||
|
* - 외부 API 호출 없음(MailSender 폴백은 로그). Ollama 외 외부 통신 금지 준수.
|
||||||
|
* - access 토큰은 ESN JwtUtil(username/role/tenantCode 3-클레임) 정책을 그대로 재사용한다.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TwoFactorService {
|
||||||
|
|
||||||
|
private static final SecureRandom RANDOM = new SecureRandom();
|
||||||
|
private static final String SYSTEM = "SYSTEM";
|
||||||
|
|
||||||
|
private final UserAuthMapper userMapper;
|
||||||
|
private final LoginVerifyMapper loginVerifyMapper;
|
||||||
|
private final JwtUtil jwtUtil;
|
||||||
|
private final MailSender mailSender;
|
||||||
|
private final UiwsProperties properties;
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return properties.getAuth().isTwofaEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 잠금 여부(1차 로그인 전 차단용). */
|
||||||
|
public boolean isLocked(EsnUser user) {
|
||||||
|
return Boolean.TRUE.equals(user.getLocked());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1차 로그인 성공 후 2차 인증 시작: verify-token 발급 + 이메일코드 발송 + 감사 기록 + 실패카운트 초기화.
|
||||||
|
* @return { verifyToken, step:"EMAIL", maskedEmail }
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Map<String, String> beginTwoFactor(EsnUser user) {
|
||||||
|
long codeValidity = properties.getAuth().getEmailCodeValiditySeconds();
|
||||||
|
long tokenValidity = properties.getAuth().getVerifyTokenValiditySeconds();
|
||||||
|
|
||||||
|
String code = String.format("%06d", RANDOM.nextInt(1_000_000));
|
||||||
|
LocalDateTime expire = LocalDateTime.now().plusSeconds(codeValidity);
|
||||||
|
|
||||||
|
// user 테이블에 코드/만료 저장(실패카운트 초기화) + 감사 이력 기록
|
||||||
|
userMapper.saveEmailCode(user.getUsername(), code, expire);
|
||||||
|
recordVerifyAttempt(user.getUsername(), code, expire);
|
||||||
|
|
||||||
|
// 이메일 발송(미설정 환경은 LogMailSender 폴백). 코드는 메일 본문에만.
|
||||||
|
String subject = "[GUARDiA ESN] 로그인 2차 인증 코드";
|
||||||
|
String body = String.format(
|
||||||
|
"안녕하세요 %s 님,\n로그인 2차 인증 코드는 [%s] 입니다.\n유효시간: %d초",
|
||||||
|
user.getUsername(), code, codeValidity);
|
||||||
|
if (user.getEmail() != null && !user.getEmail().isBlank()) {
|
||||||
|
mailSender.send(user.getEmail(), subject, body);
|
||||||
|
} else {
|
||||||
|
log.warn("[2FA] no email for user={} — code logged only", user.getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
String verifyToken = jwtUtil.generateVerifyToken(user.getUsername(), tokenValidity);
|
||||||
|
// 응답에는 코드 미포함 — verifyToken/step/maskedEmail 만.
|
||||||
|
return Map.of(
|
||||||
|
"verifyToken", verifyToken,
|
||||||
|
"step", "EMAIL",
|
||||||
|
"maskedEmail", maskEmail(user.getEmail()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2차 검증: verify-token + code 검증 → access 발급. 코드 폐기 + 감사 이력 검증완료.
|
||||||
|
* @return { token, type, username, role, tenant }
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Map<String, String> verify(String verifyToken, String code) {
|
||||||
|
String username = jwtUtil.parseVerifyTokenUsername(verifyToken);
|
||||||
|
if (username == null) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.VERIFY_TOKEN_INVALID);
|
||||||
|
}
|
||||||
|
EsnUser user = userMapper.findByUsername(username);
|
||||||
|
if (user == null) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.VERIFY_TOKEN_INVALID);
|
||||||
|
}
|
||||||
|
if (Boolean.TRUE.equals(user.getLocked())) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.ACCOUNT_LOCKED);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean codeOk = user.getEmailVerifyCode() != null
|
||||||
|
&& user.getEmailVerifyCode().equals(code)
|
||||||
|
&& user.getEmailVerifyExpire() != null
|
||||||
|
&& user.getEmailVerifyExpire().isAfter(LocalDateTime.now());
|
||||||
|
if (!codeOk) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.VERIFY_CODE_INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 코드 폐기 + 감사 검증완료 + 마지막 로그인 갱신
|
||||||
|
userMapper.clearEmailCode(username);
|
||||||
|
loginVerifyMapper.markLatestVerified(username);
|
||||||
|
userMapper.updateLastLogin(username);
|
||||||
|
|
||||||
|
String access = jwtUtil.generate(user.getUsername(), user.getRole(), user.getTenantCode());
|
||||||
|
return Map.of(
|
||||||
|
"token", access,
|
||||||
|
"type", "Bearer",
|
||||||
|
"username", user.getUsername(),
|
||||||
|
"role", user.getRole() == null ? "" : user.getRole(),
|
||||||
|
"tenant", user.getTenantCode() == null ? "" : user.getTenantCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 로그인 비밀번호 실패 시 누적/잠금 처리. */
|
||||||
|
@Transactional
|
||||||
|
public void recordLoginFailure(String username) {
|
||||||
|
userMapper.incrementLoginFail(username, properties.getAuth().getMaxLoginFail());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recordVerifyAttempt(String username, String code, LocalDateTime expire) {
|
||||||
|
UiwsLoginVerify v = new UiwsLoginVerify();
|
||||||
|
v.setUserId(username);
|
||||||
|
v.setVerifyMethod("EMAIL");
|
||||||
|
v.setVerifyCode(code);
|
||||||
|
v.setExpireAt(expire);
|
||||||
|
v.setVerifiedYn("N");
|
||||||
|
v.setCreatedBy(SYSTEM);
|
||||||
|
v.setCreatedAt(LocalDateTime.now());
|
||||||
|
loginVerifyMapper.insert(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 이메일 마스킹(자격증명 보호): ab****@domain. */
|
||||||
|
private static String maskEmail(String email) {
|
||||||
|
if (email == null || email.isBlank() || !email.contains("@")) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
int at = email.indexOf('@');
|
||||||
|
String local = email.substring(0, at);
|
||||||
|
String domain = email.substring(at);
|
||||||
|
if (local.length() <= 2) {
|
||||||
|
return local.charAt(0) + "*" + domain;
|
||||||
|
}
|
||||||
|
return local.substring(0, 2) + "*".repeat(Math.max(1, local.length() - 2)) + domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.zioinfo.esn.uiws.auth;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 2FA 로그인 검증 이력 (tb_uiws_login_verify). 원본 com.urp.uiws.domain.LoginVerify 이식. */
|
||||||
|
@Data
|
||||||
|
public class UiwsLoginVerify {
|
||||||
|
private Long verifyId;
|
||||||
|
private String userId; // ERP username 논리참조
|
||||||
|
private String verifyMethod; // EMAIL | OTP
|
||||||
|
private String verifyCode;
|
||||||
|
private LocalDateTime expireAt;
|
||||||
|
private String verifiedYn; // Y | N
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.zioinfo.esn.uiws.common;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UIWS 이식 모듈 업무 예외. RuntimeException 을 상속하여 ERP 기존 GlobalExceptionHandler
|
||||||
|
* (RuntimeException → 400 + message, 스택트레이스 미노출)에 그대로 포착된다.
|
||||||
|
* 추가 인프라/빈 없이 ERP 공통 에러 처리와 정합한다.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public class UiwsApiException extends RuntimeException {
|
||||||
|
|
||||||
|
private final UiwsErrorCode errorCode;
|
||||||
|
|
||||||
|
public UiwsApiException(UiwsErrorCode errorCode) {
|
||||||
|
super(errorCode.getMessage());
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UiwsApiException(UiwsErrorCode errorCode, String detail) {
|
||||||
|
super(detail);
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
package com.zioinfo.esn.uiws.common;
|
||||||
|
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 인증 사용자 식별자(=ERP username) 추출 헬퍼. 감사 컬럼(created_by/updated_by) 및
|
||||||
|
* 소유자(writer_id/owner_id/sender_id) 기록에 사용한다.
|
||||||
|
*
|
||||||
|
* ERP JwtFilter 는 인증 principal 로 username(String)을 세팅하므로 그 값을 그대로 사용한다.
|
||||||
|
* (UIWS 원본의 CurrentUser/UserPrincipal 패턴을 ERP 인증 모델에 맞게 단순화 이식.)
|
||||||
|
*/
|
||||||
|
public final class UiwsCurrentUser {
|
||||||
|
|
||||||
|
private static final String SYSTEM = "SYSTEM";
|
||||||
|
|
||||||
|
private UiwsCurrentUser() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 현재 사용자 ID(username). 미인증/시스템 컨텍스트는 "SYSTEM". */
|
||||||
|
public static String id() {
|
||||||
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof String username
|
||||||
|
&& !"anonymousUser".equals(username)) {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
return SYSTEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ADMIN 권한 보유 여부(데이터 가시범위 판정용 — ADMIN=전체). */
|
||||||
|
public static boolean isAdmin() {
|
||||||
|
return hasRole("ADMIN");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** MANAGER 이상(MANAGER/ADMIN) 여부 — 업무일지 댓글 권한 등에 사용. */
|
||||||
|
public static boolean isManagerOrAbove() {
|
||||||
|
return hasRole("ADMIN") || hasRole("MANAGER") || hasRole("CFO");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasRole(String role) {
|
||||||
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (auth == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return auth.getAuthorities().stream()
|
||||||
|
.anyMatch(a -> ("ROLE_" + role).equals(a.getAuthority()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package com.zioinfo.esn.uiws.common;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데이터 가시범위(스코프) — UIWS DataScopeService 의 silo 단순화 이식.
|
||||||
|
* 원본은 부서계층(TB_DEPT) 기반 팀 스코프를 산출하나, ERP 이식본은 코어 dept 테이블을 이식하지 않으므로
|
||||||
|
* 역할만으로 판정한다:
|
||||||
|
* - ADMIN → 전체(all=true)
|
||||||
|
* - 그 외 → 본인 데이터만(ownerIds = [본인])
|
||||||
|
* (부서계층 스코프가 필요해지면 ERP tb_department 연계로 확장 — 후속 트랙.)
|
||||||
|
*/
|
||||||
|
public final class UiwsDataScope {
|
||||||
|
|
||||||
|
private UiwsDataScope() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** all=true 면 전체 조회(ownerIds 무시). ownerIds 는 항상 본인 포함(빈 IN 회피). */
|
||||||
|
public record Scope(boolean all, List<String> ownerIds) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Scope current() {
|
||||||
|
String me = UiwsCurrentUser.id();
|
||||||
|
if (UiwsCurrentUser.isAdmin()) {
|
||||||
|
return new Scope(true, List.of(me));
|
||||||
|
}
|
||||||
|
return new Scope(false, List.of(me));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
package com.zioinfo.esn.uiws.common;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UIWS 이식 모듈 도메인 오류 코드.
|
||||||
|
* 원본 com.urp.uiws.common.exception.ErrorCode 의 worklog/schedule/message/auth(2FA) 영역 발췌·이식.
|
||||||
|
* 메시지는 사용자 노출용 — 스택트레이스/SQL 등 민감정보는 절대 포함하지 않는다(보안 불변규칙).
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum UiwsErrorCode {
|
||||||
|
|
||||||
|
// 공통
|
||||||
|
INVALID_REQUEST("ERR-UIWS-400", "요청이 올바르지 않습니다."),
|
||||||
|
FORBIDDEN("ERR-UIWS-403", "접근 권한이 없습니다."),
|
||||||
|
NOT_FOUND("ERR-UIWS-404", "대상을 찾을 수 없습니다."),
|
||||||
|
|
||||||
|
// worklog
|
||||||
|
WORKLOG_NOT_FOUND("ERR-UIWS-WL-404", "업무일지를 찾을 수 없습니다."),
|
||||||
|
WORKLOG_TIME_OVERLAP("ERR-UIWS-WL-409", "동일 일지 내 시간대가 중복됩니다."),
|
||||||
|
WORKLOG_TIME_INVALID("ERR-UIWS-WL-422", "근무 시작/종료 시간이 올바르지 않습니다."),
|
||||||
|
|
||||||
|
// schedule
|
||||||
|
SCHEDULE_NOT_FOUND("ERR-UIWS-SC-404", "일정을 찾을 수 없습니다."),
|
||||||
|
SCHEDULE_DT_INVALID("ERR-UIWS-SC-422", "일정 시작/종료 일시가 올바르지 않습니다."),
|
||||||
|
DIARY_NOT_FOUND("ERR-UIWS-DI-404", "일지를 찾을 수 없습니다."),
|
||||||
|
ATTACH_NOT_FOUND("ERR-UIWS-AT-404", "첨부파일을 찾을 수 없습니다."),
|
||||||
|
ATTACH_REF_TYPE_INVALID("ERR-UIWS-AT-422", "첨부 대상 유형은 SCHEDULE 또는 DIARY 여야 합니다."),
|
||||||
|
FILE_EMPTY("ERR-UIWS-FILE-400", "업로드할 파일이 비어 있습니다."),
|
||||||
|
FILE_STORAGE_ERROR("ERR-UIWS-FILE-500", "파일 저장 중 오류가 발생했습니다."),
|
||||||
|
|
||||||
|
// message
|
||||||
|
MESSAGE_NOT_FOUND("ERR-UIWS-MSG-404", "쪽지를 찾을 수 없습니다."),
|
||||||
|
MESSAGE_RCV_TYPE_INVALID("ERR-UIWS-MSG-422", "수신구분은 RECV(수신) 또는 REF(참조) 여야 합니다."),
|
||||||
|
|
||||||
|
// auth (2FA)
|
||||||
|
VERIFY_TOKEN_INVALID("ERR-UIWS-2FA-401", "2차 검증 토큰이 유효하지 않거나 만료되었습니다."),
|
||||||
|
VERIFY_CODE_INVALID("ERR-UIWS-2FA-401C", "인증 코드가 올바르지 않거나 만료되었습니다."),
|
||||||
|
ACCOUNT_LOCKED("ERR-UIWS-2FA-LOCK", "로그인 실패 횟수 초과로 계정이 잠겼습니다. 관리자에게 문의하세요."),
|
||||||
|
|
||||||
|
// system(권한관리) — UIWS system 모듈 이식
|
||||||
|
DUPLICATE_KEY("ERR-UIWS-SYS-409D", "이미 존재하는 항목입니다."),
|
||||||
|
RESOURCE_IN_USE("ERR-UIWS-SYS-409U", "참조 중인 항목이 있어 처리할 수 없습니다."),
|
||||||
|
ROLE_NOT_FOUND("ERR-UIWS-SYS-RL404", "권한을 찾을 수 없습니다."),
|
||||||
|
USER_NOT_FOUND("ERR-UIWS-SYS-US404", "사용자를 찾을 수 없습니다."),
|
||||||
|
USER_ID_DUPLICATED("ERR-UIWS-SYS-US409", "이미 사용 중인 사용자 ID입니다."),
|
||||||
|
DEPT_NOT_FOUND("ERR-UIWS-SYS-DP404", "부서를 찾을 수 없습니다."),
|
||||||
|
COMPANY_NOT_FOUND("ERR-UIWS-SYS-CO404", "거래처를 찾을 수 없습니다."),
|
||||||
|
MENU_NOT_FOUND("ERR-UIWS-SYS-MN404", "메뉴를 찾을 수 없습니다."),
|
||||||
|
PROGRAM_NOT_FOUND("ERR-UIWS-SYS-PG404", "프로그램을 찾을 수 없습니다."),
|
||||||
|
CODE_GRP_NOT_FOUND("ERR-UIWS-SYS-CG404", "코드그룹을 찾을 수 없습니다.");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
private final String message;
|
||||||
|
|
||||||
|
UiwsErrorCode(String code, String message) {
|
||||||
|
this.code = code;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package com.zioinfo.esn.uiws.common.mail;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로컬/개발/미설정용 메일 발송 폴백. 실제 전송 없이 로그로만 기록한다(외부 호출 0).
|
||||||
|
* esn.uiws.mail.mode=log (기본) 일 때 활성. SMTP 운영 시 mode=smtp 로 별도 구현 빈 활성화.
|
||||||
|
*
|
||||||
|
* 보안: 본문에 인증코드/임시비밀번호가 포함되므로 로그 레벨은 운영에서 조정.
|
||||||
|
* (인증코드는 API 응답으로는 절대 반환하지 않는다 — 메일/로그 채널로만 전달.)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@ConditionalOnProperty(name = "esn.uiws.mail.mode", havingValue = "log", matchIfMissing = true)
|
||||||
|
public class LogMailSender implements MailSender {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(String to, String subject, String body) {
|
||||||
|
log.info("[UIWS-MAIL:LOG] to={} subject={}\n{}", to, subject, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package com.zioinfo.esn.uiws.common.mail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메일 발송 추상화(UIWS 이식). 로컬/미설정 환경은 LogMailSender(로그만), 운영은 SMTP 구현으로 교체.
|
||||||
|
* 외부 API 호출은 하지 않는다(보안 불변규칙 — Ollama 외 외부 호출 금지).
|
||||||
|
*/
|
||||||
|
public interface MailSender {
|
||||||
|
|
||||||
|
void send(String to, String subject, String body);
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.zioinfo.esn.uiws.config;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UIWS 이식 모듈 설정. application.yml 의 esn.uiws.* 바인딩.
|
||||||
|
* 계획서 2FA 설정키(verify-token-validity / max-login-fail / email-code-validity) +
|
||||||
|
* 첨부 업로드 디렉터리. 모두 안전 기본값 보유(미설정이어도 동작).
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "esn.uiws")
|
||||||
|
public class UiwsProperties {
|
||||||
|
|
||||||
|
private final Auth auth = new Auth();
|
||||||
|
private final Upload upload = new Upload();
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class Auth {
|
||||||
|
/** 2FA 레이어 on/off. 기본 on(켜짐). off 시 기존 단일 JWT 로그인 흐름 유지(회귀 0). */
|
||||||
|
private boolean twofaEnabled = true;
|
||||||
|
/** 1차 통과 후 verify-token 유효시간(초). 계획서 300(5분). */
|
||||||
|
private long verifyTokenValiditySeconds = 300;
|
||||||
|
/** 이메일 인증코드 유효시간(초). 계획서 300(5분). */
|
||||||
|
private long emailCodeValiditySeconds = 300;
|
||||||
|
/** 로그인 실패 누적 N회 시 계정 잠금. 계획서 5. */
|
||||||
|
private int maxLoginFail = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class Upload {
|
||||||
|
/** 첨부파일 저장 루트. 기본 ./uploads/uiws. */
|
||||||
|
private String uploadDir = "./uploads/uiws";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
package com.zioinfo.esn.uiws.message.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.message.dto.MessageDtos.*;
|
||||||
|
import com.zioinfo.esn.uiws.message.service.MessageService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UIWS 이식 — 쪽지(모듈 03). 인증 필수(/api/messages). 원본 prefix·엔드포인트 보존.
|
||||||
|
* Spring Data Pageable 의존 회피: page/size 단순 파라미터로 이식(ERP는 spring-data-web 미사용).
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/messages")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MessageController {
|
||||||
|
|
||||||
|
private final MessageService messageService;
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<MessageSendResponse> send(@Valid @RequestBody MessageSendDto dto) {
|
||||||
|
return ApiResponse.ok(messageService.send(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sent")
|
||||||
|
public ApiResponse<MessageService.SentPage> sent(
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromDate,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toDate,
|
||||||
|
@RequestParam(required = false) String titleKeyword,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(messageService.listSent(fromDate, toDate, titleKeyword, page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sent/{id}")
|
||||||
|
public ApiResponse<SentMessageDetailDto> sentDetail(@PathVariable("id") Long id) {
|
||||||
|
return ApiResponse.ok(messageService.sentDetail(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/sent")
|
||||||
|
public ApiResponse<Void> deleteSent(@Valid @RequestBody MessageIdsRequest req) {
|
||||||
|
messageService.deleteSent(req.ids());
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/unread-count")
|
||||||
|
public ApiResponse<Long> unreadCount() {
|
||||||
|
return ApiResponse.ok(messageService.unreadCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/received")
|
||||||
|
public ApiResponse<MessageService.ReceivedPage> received(
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromDate,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toDate,
|
||||||
|
@RequestParam(required = false) String titleKeyword,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(messageService.listReceived(fromDate, toDate, titleKeyword, page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/received/{id}")
|
||||||
|
public ApiResponse<ReceivedMessageDetailDto> receivedDetail(@PathVariable("id") Long id) {
|
||||||
|
return ApiResponse.ok(messageService.receivedDetail(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/received")
|
||||||
|
public ApiResponse<Void> deleteReceived(@Valid @RequestBody MessageIdsRequest req) {
|
||||||
|
messageService.deleteReceived(req.ids());
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
package com.zioinfo.esn.uiws.message.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 쪽지(모듈 03) 요청/응답 DTO 모음. 원본 com.urp.uiws.message.dto.* 이식(필드·shape 동일).
|
||||||
|
* 프론트 경계면 계약 유지: API 응답 필드명 그대로 보존.
|
||||||
|
*/
|
||||||
|
public final class MessageDtos {
|
||||||
|
|
||||||
|
private MessageDtos() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 전송/답장 요청. */
|
||||||
|
public record MessageSendDto(
|
||||||
|
@NotBlank String title,
|
||||||
|
@NotBlank String content,
|
||||||
|
Long refWorklogId,
|
||||||
|
Long replyToId,
|
||||||
|
@Valid @NotEmpty(message = "수신자는 1명 이상이어야 합니다.") List<MessageReceiverDto> receivers
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 수신자 1건. rcvType: RECV(수신) | REF(참조). */
|
||||||
|
public record MessageReceiverDto(
|
||||||
|
@NotBlank String receiverId,
|
||||||
|
@NotBlank String rcvType
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 전송 응답. */
|
||||||
|
public record MessageSendResponse(Long messageId) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 보낸쪽지함 행. */
|
||||||
|
public record SentMessageDto(
|
||||||
|
Long messageId,
|
||||||
|
String title,
|
||||||
|
String receiverSummary,
|
||||||
|
long openCount,
|
||||||
|
long totalCount,
|
||||||
|
String sentAt
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 보낸쪽지 상세(수신자 개봉현황). */
|
||||||
|
public record SentMessageDetailDto(
|
||||||
|
Long messageId,
|
||||||
|
String title,
|
||||||
|
String content,
|
||||||
|
Long refWorklogId,
|
||||||
|
String sentAt,
|
||||||
|
List<ReceiverStatusDto> receivers,
|
||||||
|
long totalCount,
|
||||||
|
long openCount,
|
||||||
|
long unopenCount
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 수신자별 개봉현황. */
|
||||||
|
public record ReceiverStatusDto(
|
||||||
|
String receiverNm,
|
||||||
|
String rcvType,
|
||||||
|
String readYn,
|
||||||
|
String readAt
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 받은쪽지함 행. */
|
||||||
|
public record ReceivedMessageDto(
|
||||||
|
Long messageId,
|
||||||
|
String title,
|
||||||
|
String senderNm,
|
||||||
|
String sentAt,
|
||||||
|
String readYn
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 받은쪽지 상세(조회 시 개봉처리). */
|
||||||
|
public record ReceivedMessageDetailDto(
|
||||||
|
Long messageId,
|
||||||
|
String title,
|
||||||
|
String content,
|
||||||
|
String senderNm,
|
||||||
|
String sentAt,
|
||||||
|
String receivedAt,
|
||||||
|
Long refWorklogId
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 다중삭제 본문. */
|
||||||
|
public record MessageIdsRequest(
|
||||||
|
@NotEmpty(message = "ids는 필수입니다.") List<Long> ids
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
package com.zioinfo.esn.uiws.message.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.message.model.UiwsMessage;
|
||||||
|
import com.zioinfo.esn.uiws.message.model.UiwsMessageRcv;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 쪽지 MyBatis 매퍼. 원본 JPA MessageRepository/MessageRcvRepository → MyBatis 변환 이식.
|
||||||
|
* 사용자명은 esn_user(username 라벨) 조인으로 라벨화(코어 user 재사용, 별도 user 테이블 미이식).
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface MessageMapper {
|
||||||
|
|
||||||
|
// ── message 헤더
|
||||||
|
int insertMessage(UiwsMessage m);
|
||||||
|
|
||||||
|
UiwsMessage findMessageById(@Param("messageId") Long messageId);
|
||||||
|
|
||||||
|
boolean existsMessageById(@Param("messageId") Long messageId);
|
||||||
|
|
||||||
|
/** 보낸쪽지 페이징(SENDER_DEL_YN='N' 제외). */
|
||||||
|
List<UiwsMessage> searchSent(@Param("senderId") String senderId,
|
||||||
|
@Param("from") LocalDateTime from,
|
||||||
|
@Param("to") LocalDateTime to,
|
||||||
|
@Param("titleKeyword") String titleKeyword,
|
||||||
|
@Param("limit") int limit,
|
||||||
|
@Param("offset") int offset);
|
||||||
|
|
||||||
|
long countSent(@Param("senderId") String senderId,
|
||||||
|
@Param("from") LocalDateTime from,
|
||||||
|
@Param("to") LocalDateTime to,
|
||||||
|
@Param("titleKeyword") String titleKeyword);
|
||||||
|
|
||||||
|
/** 보낸쪽지 다중 소프트삭제(본인 것만). */
|
||||||
|
int softDeleteSent(@Param("senderId") String senderId,
|
||||||
|
@Param("ids") List<Long> ids,
|
||||||
|
@Param("actor") String actor);
|
||||||
|
|
||||||
|
// ── 수신자
|
||||||
|
int insertRcv(UiwsMessageRcv rcv);
|
||||||
|
|
||||||
|
List<UiwsMessageRcv> findRcvByMessageId(@Param("messageId") Long messageId);
|
||||||
|
|
||||||
|
List<UiwsMessageRcv> findRcvByMessageIds(@Param("ids") List<Long> ids);
|
||||||
|
|
||||||
|
UiwsMessageRcv findRcvByMessageAndReceiver(@Param("messageId") Long messageId,
|
||||||
|
@Param("receiverId") String receiverId);
|
||||||
|
|
||||||
|
long countUnread(@Param("receiverId") String receiverId);
|
||||||
|
|
||||||
|
/** 받은쪽지 개봉처리(READ_YN='Y', READ_AT). */
|
||||||
|
int markRead(@Param("rcvId") Long rcvId, @Param("actor") String actor, @Param("readAt") LocalDateTime readAt);
|
||||||
|
|
||||||
|
int softDeleteReceived(@Param("receiverId") String receiverId,
|
||||||
|
@Param("ids") List<Long> ids,
|
||||||
|
@Param("actor") String actor);
|
||||||
|
|
||||||
|
/** 받은쪽지 목록(조인: message + rcv). 행: messageId,title,senderId,sentAt,readYn. */
|
||||||
|
List<Map<String, Object>> searchReceived(@Param("receiverId") String receiverId,
|
||||||
|
@Param("from") LocalDateTime from,
|
||||||
|
@Param("to") LocalDateTime to,
|
||||||
|
@Param("titleKeyword") String titleKeyword,
|
||||||
|
@Param("limit") int limit,
|
||||||
|
@Param("offset") int offset);
|
||||||
|
|
||||||
|
long countReceived(@Param("receiverId") String receiverId,
|
||||||
|
@Param("from") LocalDateTime from,
|
||||||
|
@Param("to") LocalDateTime to,
|
||||||
|
@Param("titleKeyword") String titleKeyword);
|
||||||
|
|
||||||
|
// ── 사용자명 라벨(코어 user 재사용)
|
||||||
|
List<Map<String, String>> findUserNames(@Param("ids") List<String> ids);
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.zioinfo.esn.uiws.message.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 쪽지 헤더 (tb_uiws_message). 원본 com.urp.uiws.domain.Message 이식.
|
||||||
|
* MyBatis map-underscore-to-camel-case 로 컬럼↔필드 자동 매핑.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class UiwsMessage {
|
||||||
|
private Long messageId;
|
||||||
|
private String senderId;
|
||||||
|
private String title;
|
||||||
|
private String content;
|
||||||
|
private Long refWorklogId;
|
||||||
|
private Long replyToId;
|
||||||
|
private LocalDateTime sentAt;
|
||||||
|
private String senderDelYn;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.zioinfo.esn.uiws.message.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 쪽지 수신자 (tb_uiws_message_rcv). 원본 com.urp.uiws.domain.MessageRcv 이식.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class UiwsMessageRcv {
|
||||||
|
private Long rcvId;
|
||||||
|
private Long messageId;
|
||||||
|
private String receiverId;
|
||||||
|
private String rcvType; // RECV | REF
|
||||||
|
private String readYn; // Y | N
|
||||||
|
private LocalDateTime readAt;
|
||||||
|
private String receiverDelYn; // Y | N
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,271 @@
|
|||||||
|
package com.zioinfo.esn.uiws.message.service;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsApiException;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsCurrentUser;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsErrorCode;
|
||||||
|
import com.zioinfo.esn.uiws.message.dto.MessageDtos.*;
|
||||||
|
import com.zioinfo.esn.uiws.message.mapper.MessageMapper;
|
||||||
|
import com.zioinfo.esn.uiws.message.model.UiwsMessage;
|
||||||
|
import com.zioinfo.esn.uiws.message.model.UiwsMessageRcv;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 쪽지(모듈 03) 서비스 — 원본 com.urp.uiws.message.service.MessageService 를 MyBatis 로 변환 이식.
|
||||||
|
* 로직(전송/답장, 보낸함, 개봉현황, 받은함, 개봉처리, 다중삭제, 미열람 배지)을 동등하게 보존한다.
|
||||||
|
* 기본 조회기간 = 최근 1주일.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MessageService {
|
||||||
|
|
||||||
|
private static final Set<String> VALID_RCV_TYPES = Set.of("RECV", "REF");
|
||||||
|
private static final DateTimeFormatter DT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
|
||||||
|
|
||||||
|
private final MessageMapper mapper;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ 전송/답장
|
||||||
|
@Transactional
|
||||||
|
public MessageSendResponse send(MessageSendDto dto) {
|
||||||
|
String actor = UiwsCurrentUser.id();
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
if (dto.replyToId() != null && !mapper.existsMessageById(dto.replyToId())) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.MESSAGE_NOT_FOUND, "답장 원본 쪽지를 찾을 수 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
UiwsMessage m = new UiwsMessage();
|
||||||
|
m.setSenderId(actor);
|
||||||
|
m.setTitle(dto.title());
|
||||||
|
m.setContent(dto.content());
|
||||||
|
m.setRefWorklogId(dto.refWorklogId());
|
||||||
|
m.setReplyToId(dto.replyToId());
|
||||||
|
m.setSentAt(now);
|
||||||
|
m.setSenderDelYn("N");
|
||||||
|
m.setCreatedBy(actor);
|
||||||
|
m.setCreatedAt(now);
|
||||||
|
mapper.insertMessage(m); // useGeneratedKeys → m.messageId
|
||||||
|
|
||||||
|
// 수신자: 동일 수신자 중복 제거(UNIQUE(message_id,receiver_id) 보호). 첫 등장 rcvType 채택.
|
||||||
|
Set<String> seen = new LinkedHashSet<>();
|
||||||
|
for (MessageReceiverDto r : dto.receivers()) {
|
||||||
|
String receiverId = (r.receiverId() == null) ? null : r.receiverId().trim();
|
||||||
|
String rcvType = (r.rcvType() == null) ? null : r.rcvType().trim().toUpperCase();
|
||||||
|
if (receiverId == null || receiverId.isBlank()) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "수신자 ID가 비어 있습니다.");
|
||||||
|
}
|
||||||
|
if (!VALID_RCV_TYPES.contains(rcvType)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.MESSAGE_RCV_TYPE_INVALID);
|
||||||
|
}
|
||||||
|
if (!seen.add(receiverId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
UiwsMessageRcv rcv = new UiwsMessageRcv();
|
||||||
|
rcv.setMessageId(m.getMessageId());
|
||||||
|
rcv.setReceiverId(receiverId);
|
||||||
|
rcv.setRcvType(rcvType);
|
||||||
|
rcv.setReadYn("N");
|
||||||
|
rcv.setReceiverDelYn("N");
|
||||||
|
rcv.setCreatedBy(actor);
|
||||||
|
rcv.setCreatedAt(now);
|
||||||
|
mapper.insertRcv(rcv);
|
||||||
|
}
|
||||||
|
return new MessageSendResponse(m.getMessageId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ 보낸쪽지함
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public SentPage listSent(LocalDate fromDate, LocalDate toDate, String titleKeyword, int page, int size) {
|
||||||
|
LocalDateTime[] range = range(fromDate, toDate);
|
||||||
|
String me = UiwsCurrentUser.id();
|
||||||
|
String kw = blankToNull(titleKeyword);
|
||||||
|
long total = mapper.countSent(me, range[0], range[1], kw);
|
||||||
|
List<UiwsMessage> rows = mapper.searchSent(me, range[0], range[1], kw, size, page * size);
|
||||||
|
|
||||||
|
List<Long> ids = rows.stream().map(UiwsMessage::getMessageId).toList();
|
||||||
|
List<UiwsMessageRcv> rcvs = ids.isEmpty() ? List.of() : mapper.findRcvByMessageIds(ids);
|
||||||
|
Map<Long, List<UiwsMessageRcv>> byMsg = rcvs.stream().collect(Collectors.groupingBy(UiwsMessageRcv::getMessageId));
|
||||||
|
Map<String, String> names = userNames(rcvs.stream().map(UiwsMessageRcv::getReceiverId).toList());
|
||||||
|
|
||||||
|
List<SentMessageDto> content = rows.stream().map(m -> {
|
||||||
|
List<UiwsMessageRcv> list = byMsg.getOrDefault(m.getMessageId(), List.of());
|
||||||
|
long openCount = list.stream().filter(r -> "Y".equals(r.getReadYn())).count();
|
||||||
|
return new SentMessageDto(m.getMessageId(), m.getTitle(),
|
||||||
|
receiverSummary(list, names), openCount, list.size(), fmt(m.getSentAt()));
|
||||||
|
}).toList();
|
||||||
|
return new SentPage(content, total, page, size, totalPages(total, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ 보낸쪽지 상세
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public SentMessageDetailDto sentDetail(Long messageId) {
|
||||||
|
String me = UiwsCurrentUser.id();
|
||||||
|
UiwsMessage m = mapper.findMessageById(messageId);
|
||||||
|
if (m == null) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.MESSAGE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (!me.equals(m.getSenderId())) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.FORBIDDEN, "본인이 보낸 쪽지만 조회할 수 있습니다.");
|
||||||
|
}
|
||||||
|
List<UiwsMessageRcv> list = mapper.findRcvByMessageId(messageId);
|
||||||
|
Map<String, String> names = userNames(list.stream().map(UiwsMessageRcv::getReceiverId).toList());
|
||||||
|
|
||||||
|
List<ReceiverStatusDto> receivers = list.stream()
|
||||||
|
.map(r -> new ReceiverStatusDto(
|
||||||
|
names.getOrDefault(r.getReceiverId(), r.getReceiverId()),
|
||||||
|
r.getRcvType(), r.getReadYn(), fmt(r.getReadAt())))
|
||||||
|
.toList();
|
||||||
|
long total = receivers.size();
|
||||||
|
long open = list.stream().filter(r -> "Y".equals(r.getReadYn())).count();
|
||||||
|
return new SentMessageDetailDto(m.getMessageId(), m.getTitle(), m.getContent(), m.getRefWorklogId(),
|
||||||
|
fmt(m.getSentAt()), receivers, total, open, total - open);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ 보낸쪽지 다중삭제
|
||||||
|
@Transactional
|
||||||
|
public void deleteSent(List<Long> ids) {
|
||||||
|
if (ids == null || ids.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String me = UiwsCurrentUser.id();
|
||||||
|
mapper.softDeleteSent(me, ids, me);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ 미열람 수(배지)
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public long unreadCount() {
|
||||||
|
return mapper.countUnread(UiwsCurrentUser.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ 받은쪽지함
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public ReceivedPage listReceived(LocalDate fromDate, LocalDate toDate, String titleKeyword, int page, int size) {
|
||||||
|
LocalDateTime[] range = range(fromDate, toDate);
|
||||||
|
String me = UiwsCurrentUser.id();
|
||||||
|
String kw = blankToNull(titleKeyword);
|
||||||
|
long total = mapper.countReceived(me, range[0], range[1], kw);
|
||||||
|
List<Map<String, Object>> rows = mapper.searchReceived(me, range[0], range[1], kw, size, page * size);
|
||||||
|
Map<String, String> names = userNames(rows.stream().map(r -> str(r.get("senderId"))).toList());
|
||||||
|
|
||||||
|
List<ReceivedMessageDto> content = rows.stream()
|
||||||
|
.map(r -> new ReceivedMessageDto(
|
||||||
|
toLongObj(r.get("messageId")),
|
||||||
|
str(r.get("title")),
|
||||||
|
names.getOrDefault(str(r.get("senderId")), str(r.get("senderId"))),
|
||||||
|
fmt(toDt(r.get("sentAt"))),
|
||||||
|
str(r.get("readYn"))))
|
||||||
|
.toList();
|
||||||
|
return new ReceivedPage(content, total, page, size, totalPages(total, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ 받은쪽지 상세(개봉처리)
|
||||||
|
@Transactional
|
||||||
|
public ReceivedMessageDetailDto receivedDetail(Long messageId) {
|
||||||
|
String me = UiwsCurrentUser.id();
|
||||||
|
UiwsMessageRcv rcv = mapper.findRcvByMessageAndReceiver(messageId, me);
|
||||||
|
if (rcv == null || "Y".equals(rcv.getReceiverDelYn())) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.MESSAGE_NOT_FOUND, "받은 쪽지를 찾을 수 없습니다.");
|
||||||
|
}
|
||||||
|
UiwsMessage m = mapper.findMessageById(messageId);
|
||||||
|
if (m == null) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.MESSAGE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
LocalDateTime readAt = rcv.getReadAt();
|
||||||
|
if (!"Y".equals(rcv.getReadYn())) {
|
||||||
|
readAt = LocalDateTime.now();
|
||||||
|
mapper.markRead(rcv.getRcvId(), me, readAt);
|
||||||
|
}
|
||||||
|
String senderNm = userNames(List.of(m.getSenderId())).getOrDefault(m.getSenderId(), m.getSenderId());
|
||||||
|
return new ReceivedMessageDetailDto(m.getMessageId(), m.getTitle(), m.getContent(),
|
||||||
|
senderNm, fmt(m.getSentAt()), fmt(readAt), m.getRefWorklogId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ 받은쪽지 다중삭제
|
||||||
|
@Transactional
|
||||||
|
public void deleteReceived(List<Long> ids) {
|
||||||
|
if (ids == null || ids.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String me = UiwsCurrentUser.id();
|
||||||
|
mapper.softDeleteReceived(me, ids, me);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================== helpers
|
||||||
|
|
||||||
|
private LocalDateTime[] range(LocalDate fromDate, LocalDate toDate) {
|
||||||
|
LocalDate to = (toDate != null) ? toDate : LocalDate.now();
|
||||||
|
LocalDate from = (fromDate != null) ? fromDate : to.minusWeeks(1);
|
||||||
|
return new LocalDateTime[]{from.atStartOfDay(), to.atTime(LocalTime.MAX)};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String receiverSummary(List<UiwsMessageRcv> list, Map<String, String> names) {
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String first = names.getOrDefault(list.get(0).getReceiverId(), list.get(0).getReceiverId());
|
||||||
|
return list.size() == 1 ? first : first + " 외 " + (list.size() - 1) + "명";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> userNames(List<String> userIds) {
|
||||||
|
List<String> ids = userIds.stream().filter(s -> s != null && !s.isBlank()).distinct().toList();
|
||||||
|
if (ids.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
return mapper.findUserNames(ids).stream()
|
||||||
|
.collect(Collectors.toMap(r -> r.get("username"), r -> r.get("fullName"), (a, b) -> a));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int totalPages(long total, int size) {
|
||||||
|
return size <= 0 ? 0 : (int) ((total + size - 1) / size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String blankToNull(String s) {
|
||||||
|
return (s == null || s.isBlank()) ? null : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String str(Object o) {
|
||||||
|
return o == null ? "" : o.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Long toLongObj(Object o) {
|
||||||
|
if (o == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (o instanceof Number n) ? n.longValue() : Long.parseLong(o.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalDateTime toDt(Object o) {
|
||||||
|
if (o == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (o instanceof LocalDateTime dt) {
|
||||||
|
return dt;
|
||||||
|
}
|
||||||
|
if (o instanceof java.sql.Timestamp ts) {
|
||||||
|
return ts.toLocalDateTime();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String fmt(LocalDateTime dt) {
|
||||||
|
return dt != null ? dt.format(DT) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 페이지 응답(ERP ApiResponse 안에 그대로 직렬화 — content/totalElements/page/size/totalPages 보존)
|
||||||
|
public record SentPage(List<SentMessageDto> content, long totalElements, int page, int size, int totalPages) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ReceivedPage(List<ReceivedMessageDto> content, long totalElements, int page, int size, int totalPages) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
package com.zioinfo.esn.uiws.schedule.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.schedule.dto.ScheduleDtos.AttachmentDto;
|
||||||
|
import com.zioinfo.esn.uiws.schedule.service.AttachmentService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.core.io.FileSystemResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.ContentDisposition;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UIWS 이식 — 첨부파일(모듈 02, 폴리모픽 SCHEDULE/DIARY). 인증 필수(/api/attachments).
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/attachments")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AttachmentController {
|
||||||
|
|
||||||
|
private final AttachmentService attachmentService;
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<AttachmentDto> upload(
|
||||||
|
@RequestParam("refType") String refType,
|
||||||
|
@RequestParam("refId") Long refId,
|
||||||
|
@RequestParam("file") MultipartFile file) {
|
||||||
|
return ApiResponse.ok(attachmentService.upload(refType, refId, file));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/download")
|
||||||
|
public ResponseEntity<Resource> download(@PathVariable("id") Long id) {
|
||||||
|
AttachmentService.DownloadFile f = attachmentService.download(id);
|
||||||
|
Resource resource = new FileSystemResource(f.path());
|
||||||
|
String contentType;
|
||||||
|
try {
|
||||||
|
contentType = Files.probeContentType(f.path());
|
||||||
|
} catch (IOException e) {
|
||||||
|
contentType = null;
|
||||||
|
}
|
||||||
|
ContentDisposition cd = ContentDisposition.attachment()
|
||||||
|
.filename(f.fileNm(), StandardCharsets.UTF_8)
|
||||||
|
.build();
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentDisposition(cd);
|
||||||
|
long len = f.path().toFile().length();
|
||||||
|
if (len > 0) {
|
||||||
|
headers.setContentLength(len);
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.headers(headers)
|
||||||
|
.contentType(contentType != null ? MediaType.parseMediaType(contentType) : MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
.body(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ApiResponse<Void> delete(@PathVariable("id") Long id) {
|
||||||
|
attachmentService.delete(id);
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
package com.zioinfo.esn.uiws.schedule.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.schedule.dto.ScheduleDtos.*;
|
||||||
|
import com.zioinfo.esn.uiws.schedule.service.DiaryService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UIWS 이식 — 일지(모듈 02). 인증 필수(/api/diaries).
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/diaries")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DiaryController {
|
||||||
|
|
||||||
|
private final DiaryService diaryService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<DiaryPage> list(
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromDate,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toDate,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(diaryService.list(fromDate, toDate, page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<DiaryDetailDto> create(@Valid @RequestBody DiarySaveDto dto) {
|
||||||
|
return ApiResponse.ok(diaryService.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<DiaryDetailDto> detail(@PathVariable("id") Long id) {
|
||||||
|
return ApiResponse.ok(diaryService.detail(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ApiResponse<DiaryDetailDto> update(@PathVariable("id") Long id, @Valid @RequestBody DiarySaveDto dto) {
|
||||||
|
return ApiResponse.ok(diaryService.update(id, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ApiResponse<Void> delete(@PathVariable("id") Long id) {
|
||||||
|
diaryService.delete(id);
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
package com.zioinfo.esn.uiws.schedule.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.schedule.dto.ScheduleDtos.*;
|
||||||
|
import com.zioinfo.esn.uiws.schedule.service.ScheduleService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UIWS 이식 — 일정(모듈 02). 인증 필수(/api/schedules). 원본 엔드포인트 보존.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/schedules")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ScheduleController {
|
||||||
|
|
||||||
|
private final ScheduleService scheduleService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<List<ScheduleDto>> calendar(
|
||||||
|
@RequestParam(defaultValue = "PERSONAL") String type,
|
||||||
|
@RequestParam(defaultValue = "month") String view,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate baseDate,
|
||||||
|
@RequestParam(required = false) String deptId) {
|
||||||
|
return ApiResponse.ok(scheduleService.calendar(type, view, baseDate, deptId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/all")
|
||||||
|
public ApiResponse<SchedulePage> all(
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromDate,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toDate,
|
||||||
|
@RequestParam(required = false) String type,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(scheduleService.all(fromDate, toDate, type, page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/search")
|
||||||
|
public ApiResponse<List<ScheduleDto>> search(@RequestParam(required = false) String keyword) {
|
||||||
|
return ApiResponse.ok(scheduleService.search(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<ScheduleDto> create(@Valid @RequestBody ScheduleSaveDto dto) {
|
||||||
|
return ApiResponse.ok(scheduleService.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<ScheduleDetailDto> detail(@PathVariable("id") Long id) {
|
||||||
|
return ApiResponse.ok(scheduleService.detail(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ApiResponse<ScheduleDetailDto> update(@PathVariable("id") Long id, @Valid @RequestBody ScheduleSaveDto dto) {
|
||||||
|
return ApiResponse.ok(scheduleService.update(id, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ApiResponse<Void> delete(@PathVariable("id") Long id) {
|
||||||
|
scheduleService.delete(id);
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
package com.zioinfo.esn.uiws.schedule.dto;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.schedule.model.UiwsAttach;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일정·일지·첨부(모듈 02) DTO. 원본 com.urp.uiws.schedule.dto.* 이식(shape 동일).
|
||||||
|
*/
|
||||||
|
public final class ScheduleDtos {
|
||||||
|
|
||||||
|
private ScheduleDtos() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ScheduleDto(
|
||||||
|
Long scheduleId,
|
||||||
|
String scheType,
|
||||||
|
String title,
|
||||||
|
String scheGubunCd,
|
||||||
|
String importanceCd,
|
||||||
|
String startDt,
|
||||||
|
String endDt,
|
||||||
|
String ownerId,
|
||||||
|
String deptId
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ScheduleDetailDto(
|
||||||
|
Long scheduleId,
|
||||||
|
String scheType,
|
||||||
|
String title,
|
||||||
|
String scheGubunCd,
|
||||||
|
String importanceCd,
|
||||||
|
String startDt,
|
||||||
|
String endDt,
|
||||||
|
String ownerId,
|
||||||
|
String deptId,
|
||||||
|
String content,
|
||||||
|
String chargerId,
|
||||||
|
String chargerNm,
|
||||||
|
List<AttachmentDto> attachments
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ScheduleSaveDto(
|
||||||
|
@NotBlank @Pattern(regexp = "PERSONAL|DEPT") String scheType,
|
||||||
|
@NotBlank String title,
|
||||||
|
String scheGubunCd,
|
||||||
|
String importanceCd,
|
||||||
|
@NotNull String startDt,
|
||||||
|
@NotNull String endDt,
|
||||||
|
String content,
|
||||||
|
String deptId,
|
||||||
|
String chargerId,
|
||||||
|
List<Long> attachmentIds
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DiaryDto(
|
||||||
|
Long diaryId,
|
||||||
|
String title,
|
||||||
|
String writerNm,
|
||||||
|
String diaryDate,
|
||||||
|
Long scheduleId
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DiaryDetailDto(
|
||||||
|
Long diaryId,
|
||||||
|
String title,
|
||||||
|
String writerNm,
|
||||||
|
String diaryDate,
|
||||||
|
Long scheduleId,
|
||||||
|
String content,
|
||||||
|
List<AttachmentDto> attachments
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DiarySaveDto(
|
||||||
|
@NotBlank String title,
|
||||||
|
String content,
|
||||||
|
Long scheduleId,
|
||||||
|
String diaryDate,
|
||||||
|
List<Long> attachmentIds
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record AttachmentDto(
|
||||||
|
Long attachId,
|
||||||
|
String refType,
|
||||||
|
Long refId,
|
||||||
|
String fileNm,
|
||||||
|
Long fileSize
|
||||||
|
) {
|
||||||
|
public static AttachmentDto from(UiwsAttach a) {
|
||||||
|
return new AttachmentDto(a.getAttachId(), a.getRefType(), a.getRefId(), a.getFileNm(), a.getFileSize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 페이지 래퍼(content/totalElements/page/size/totalPages). */
|
||||||
|
public record DiaryPage(List<DiaryDto> content, long totalElements, int page, int size, int totalPages) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SchedulePage(List<ScheduleDto> content, long totalElements, int page, int size, int totalPages) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
package com.zioinfo.esn.uiws.schedule.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.schedule.model.UiwsAttach;
|
||||||
|
import com.zioinfo.esn.uiws.schedule.model.UiwsDiary;
|
||||||
|
import com.zioinfo.esn.uiws.schedule.model.UiwsSchedule;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일정·일지·첨부 MyBatis 매퍼. 원본 JPA ScheduleRepository/DiaryRepository/AttachRepository 변환 이식.
|
||||||
|
* 데이터 가시범위: scopeAll(true=ADMIN 전체) OR owner IN (ownerIds). ownerIds 는 항상 본인 포함(빈 IN 회피).
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface ScheduleMapper {
|
||||||
|
|
||||||
|
// ── schedule
|
||||||
|
int insertSchedule(UiwsSchedule s);
|
||||||
|
|
||||||
|
int updateSchedule(UiwsSchedule s);
|
||||||
|
|
||||||
|
UiwsSchedule findScheduleById(@Param("scheduleId") Long scheduleId);
|
||||||
|
|
||||||
|
boolean existsScheduleById(@Param("scheduleId") Long scheduleId);
|
||||||
|
|
||||||
|
int deleteScheduleById(@Param("scheduleId") Long scheduleId);
|
||||||
|
|
||||||
|
/** 달력: 기간 겹침([from,to) 배타 상한) + type + 개인소유/부서필터. */
|
||||||
|
List<UiwsSchedule> findInRange(@Param("from") LocalDateTime from,
|
||||||
|
@Param("to") LocalDateTime to,
|
||||||
|
@Param("scheType") String scheType,
|
||||||
|
@Param("ownerId") String ownerId,
|
||||||
|
@Param("deptId") String deptId);
|
||||||
|
|
||||||
|
List<UiwsSchedule> searchAll(@Param("from") LocalDateTime from,
|
||||||
|
@Param("to") LocalDateTime to,
|
||||||
|
@Param("scheType") String scheType,
|
||||||
|
@Param("scopeAll") boolean scopeAll,
|
||||||
|
@Param("ownerIds") List<String> ownerIds,
|
||||||
|
@Param("limit") int limit,
|
||||||
|
@Param("offset") int offset);
|
||||||
|
|
||||||
|
long countAll(@Param("from") LocalDateTime from,
|
||||||
|
@Param("to") LocalDateTime to,
|
||||||
|
@Param("scheType") String scheType,
|
||||||
|
@Param("scopeAll") boolean scopeAll,
|
||||||
|
@Param("ownerIds") List<String> ownerIds);
|
||||||
|
|
||||||
|
List<UiwsSchedule> searchPopup(@Param("keyword") String keyword,
|
||||||
|
@Param("scopeAll") boolean scopeAll,
|
||||||
|
@Param("ownerIds") List<String> ownerIds);
|
||||||
|
|
||||||
|
// ── diary
|
||||||
|
int insertDiary(UiwsDiary d);
|
||||||
|
|
||||||
|
int updateDiary(UiwsDiary d);
|
||||||
|
|
||||||
|
UiwsDiary findDiaryById(@Param("diaryId") Long diaryId);
|
||||||
|
|
||||||
|
boolean existsDiaryById(@Param("diaryId") Long diaryId);
|
||||||
|
|
||||||
|
int deleteDiaryById(@Param("diaryId") Long diaryId);
|
||||||
|
|
||||||
|
List<UiwsDiary> searchDiary(@Param("from") LocalDate from,
|
||||||
|
@Param("to") LocalDate to,
|
||||||
|
@Param("limit") int limit,
|
||||||
|
@Param("offset") int offset);
|
||||||
|
|
||||||
|
long countDiary(@Param("from") LocalDate from, @Param("to") LocalDate to);
|
||||||
|
|
||||||
|
// ── attach
|
||||||
|
int insertAttach(UiwsAttach a);
|
||||||
|
|
||||||
|
UiwsAttach findAttachById(@Param("attachId") Long attachId);
|
||||||
|
|
||||||
|
int updateAttachRef(@Param("attachId") Long attachId,
|
||||||
|
@Param("refType") String refType,
|
||||||
|
@Param("refId") Long refId,
|
||||||
|
@Param("actor") String actor);
|
||||||
|
|
||||||
|
List<UiwsAttach> findAttachByRef(@Param("refType") String refType, @Param("refId") Long refId);
|
||||||
|
|
||||||
|
int deleteAttachById(@Param("attachId") Long attachId);
|
||||||
|
|
||||||
|
int deleteAttachByRef(@Param("refType") String refType, @Param("refId") Long refId);
|
||||||
|
|
||||||
|
// ── 사용자명 라벨(코어 user 재사용)
|
||||||
|
List<Map<String, String>> findUserNames(@Param("ids") List<String> ids);
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.zioinfo.esn.uiws.schedule.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 첨부파일 폴리모픽 (tb_uiws_attach, ref_type=SCHEDULE|DIARY). 원본 com.urp.uiws.domain.Attach 이식. */
|
||||||
|
@Data
|
||||||
|
public class UiwsAttach {
|
||||||
|
private Long attachId;
|
||||||
|
private String refType;
|
||||||
|
private Long refId;
|
||||||
|
private String fileNm;
|
||||||
|
private String filePath;
|
||||||
|
private Long fileSize;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package com.zioinfo.esn.uiws.schedule.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 일지 (tb_uiws_diary). 원본 com.urp.uiws.domain.Diary 이식. */
|
||||||
|
@Data
|
||||||
|
public class UiwsDiary {
|
||||||
|
private Long diaryId;
|
||||||
|
private String title;
|
||||||
|
private String content;
|
||||||
|
private Long scheduleId;
|
||||||
|
private String writerId;
|
||||||
|
private LocalDate diaryDate;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.zioinfo.esn.uiws.schedule.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 일정 (tb_uiws_schedule). 원본 com.urp.uiws.domain.Schedule 이식. */
|
||||||
|
@Data
|
||||||
|
public class UiwsSchedule {
|
||||||
|
private Long scheduleId;
|
||||||
|
private String scheType; // PERSONAL | DEPT
|
||||||
|
private String title;
|
||||||
|
private String scheGubunCd;
|
||||||
|
private String importanceCd;
|
||||||
|
private LocalDateTime startDt;
|
||||||
|
private LocalDateTime endDt;
|
||||||
|
private String content;
|
||||||
|
private String ownerId;
|
||||||
|
private String deptId;
|
||||||
|
private String chargerId;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,121 @@
|
|||||||
|
package com.zioinfo.esn.uiws.schedule.service;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsApiException;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsCurrentUser;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsErrorCode;
|
||||||
|
import com.zioinfo.esn.uiws.schedule.dto.ScheduleDtos.AttachmentDto;
|
||||||
|
import com.zioinfo.esn.uiws.schedule.mapper.ScheduleMapper;
|
||||||
|
import com.zioinfo.esn.uiws.schedule.model.UiwsAttach;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 첨부파일(폴리모픽 SCHEDULE/DIARY) 업로드·삭제·연결. 원본 com.urp.uiws.schedule.service.AttachmentService 이식.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AttachmentService {
|
||||||
|
|
||||||
|
public static final String REF_SCHEDULE = "SCHEDULE";
|
||||||
|
public static final String REF_DIARY = "DIARY";
|
||||||
|
private static final Set<String> VALID_REF_TYPES = Set.of(REF_SCHEDULE, REF_DIARY);
|
||||||
|
|
||||||
|
private final ScheduleMapper mapper;
|
||||||
|
private final FileStorageService fileStorageService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public AttachmentDto upload(String refType, Long refId, MultipartFile file) {
|
||||||
|
String type = normalizeRefType(refType);
|
||||||
|
if (refId == null) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "refId 는 필수입니다.");
|
||||||
|
}
|
||||||
|
FileStorageService.StoredFile stored = fileStorageService.store(file);
|
||||||
|
|
||||||
|
String actor = UiwsCurrentUser.id();
|
||||||
|
UiwsAttach a = new UiwsAttach();
|
||||||
|
a.setRefType(type);
|
||||||
|
a.setRefId(refId);
|
||||||
|
a.setFileNm(stored.originalName());
|
||||||
|
a.setFilePath(stored.relativePath());
|
||||||
|
a.setFileSize(stored.size());
|
||||||
|
a.setCreatedBy(actor);
|
||||||
|
a.setCreatedAt(LocalDateTime.now());
|
||||||
|
mapper.insertAttach(a);
|
||||||
|
return AttachmentDto.from(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DownloadFile(String fileNm, Path path) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public DownloadFile download(Long attachId) {
|
||||||
|
UiwsAttach a = mapper.findAttachById(attachId);
|
||||||
|
if (a == null) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.ATTACH_NOT_FOUND);
|
||||||
|
}
|
||||||
|
Path path = fileStorageService.resolve(a.getFilePath());
|
||||||
|
if (!Files.exists(path)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.ATTACH_NOT_FOUND, "파일이 존재하지 않습니다.");
|
||||||
|
}
|
||||||
|
return new DownloadFile(a.getFileNm(), path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(Long attachId) {
|
||||||
|
UiwsAttach a = mapper.findAttachById(attachId);
|
||||||
|
if (a == null) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.ATTACH_NOT_FOUND);
|
||||||
|
}
|
||||||
|
fileStorageService.delete(a.getFilePath());
|
||||||
|
mapper.deleteAttachById(attachId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 일정/일지 저장 시 attachmentIds 의 첨부들을 해당 대상으로 귀속(확정). */
|
||||||
|
@Transactional
|
||||||
|
public void link(String refType, Long refId, List<Long> attachmentIds) {
|
||||||
|
String type = normalizeRefType(refType);
|
||||||
|
if (attachmentIds == null || attachmentIds.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String actor = UiwsCurrentUser.id();
|
||||||
|
for (Long id : attachmentIds) {
|
||||||
|
if (id == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
mapper.updateAttachRef(id, type, refId, actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<AttachmentDto> list(String refType, Long refId) {
|
||||||
|
return mapper.findAttachByRef(normalizeRefType(refType), refId)
|
||||||
|
.stream().map(AttachmentDto::from).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 대상 삭제 시 귀속 첨부 일괄 제거(물리파일 포함). */
|
||||||
|
@Transactional
|
||||||
|
public void deleteByRef(String refType, Long refId) {
|
||||||
|
String type = normalizeRefType(refType);
|
||||||
|
List<UiwsAttach> rows = mapper.findAttachByRef(type, refId);
|
||||||
|
for (UiwsAttach a : rows) {
|
||||||
|
fileStorageService.delete(a.getFilePath());
|
||||||
|
}
|
||||||
|
mapper.deleteAttachByRef(type, refId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeRefType(String refType) {
|
||||||
|
String type = refType == null ? "" : refType.trim().toUpperCase();
|
||||||
|
if (!VALID_REF_TYPES.contains(type)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.ATTACH_REF_TYPE_INVALID);
|
||||||
|
}
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,133 @@
|
|||||||
|
package com.zioinfo.esn.uiws.schedule.service;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsApiException;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsCurrentUser;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsErrorCode;
|
||||||
|
import com.zioinfo.esn.uiws.schedule.dto.ScheduleDtos.*;
|
||||||
|
import com.zioinfo.esn.uiws.schedule.mapper.ScheduleMapper;
|
||||||
|
import com.zioinfo.esn.uiws.schedule.model.UiwsDiary;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일지(모듈 02) 서비스 — 원본 com.urp.uiws.schedule.service.DiaryService MyBatis 변환 이식.
|
||||||
|
* 목록(페이징)·CRUD·첨부 연계.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DiaryService {
|
||||||
|
|
||||||
|
private final ScheduleMapper mapper;
|
||||||
|
private final AttachmentService attachmentService;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public DiaryPage list(LocalDate fromDate, LocalDate toDate, int page, int size) {
|
||||||
|
LocalDate to = (toDate != null) ? toDate : LocalDate.now();
|
||||||
|
LocalDate from = (fromDate != null) ? fromDate : to.minusMonths(1);
|
||||||
|
long total = mapper.countDiary(from, to);
|
||||||
|
List<UiwsDiary> rows = mapper.searchDiary(from, to, size, page * size);
|
||||||
|
Map<String, String> names = userNames(rows.stream().map(UiwsDiary::getWriterId).toList());
|
||||||
|
List<DiaryDto> content = rows.stream()
|
||||||
|
.map(d -> new DiaryDto(d.getDiaryId(), d.getTitle(),
|
||||||
|
names.getOrDefault(d.getWriterId(), d.getWriterId()),
|
||||||
|
d.getDiaryDate() != null ? d.getDiaryDate().toString() : null,
|
||||||
|
d.getScheduleId()))
|
||||||
|
.toList();
|
||||||
|
return new DiaryPage(content, total, page, size, totalPages(total, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public DiaryDetailDto create(DiarySaveDto dto) {
|
||||||
|
String actor = UiwsCurrentUser.id();
|
||||||
|
UiwsDiary d = new UiwsDiary();
|
||||||
|
applySave(d, dto);
|
||||||
|
d.setWriterId(actor);
|
||||||
|
d.setCreatedBy(actor);
|
||||||
|
d.setCreatedAt(LocalDateTime.now());
|
||||||
|
mapper.insertDiary(d);
|
||||||
|
attachmentService.link(AttachmentService.REF_DIARY, d.getDiaryId(), dto.attachmentIds());
|
||||||
|
return toDetail(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public DiaryDetailDto detail(Long id) {
|
||||||
|
return toDetail(find(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public DiaryDetailDto update(Long id, DiarySaveDto dto) {
|
||||||
|
String actor = UiwsCurrentUser.id();
|
||||||
|
UiwsDiary d = find(id);
|
||||||
|
applySave(d, dto);
|
||||||
|
d.setUpdatedBy(actor);
|
||||||
|
d.setUpdatedAt(LocalDateTime.now());
|
||||||
|
mapper.updateDiary(d);
|
||||||
|
attachmentService.link(AttachmentService.REF_DIARY, d.getDiaryId(), dto.attachmentIds());
|
||||||
|
return toDetail(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(Long id) {
|
||||||
|
if (!mapper.existsDiaryById(id)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.DIARY_NOT_FOUND);
|
||||||
|
}
|
||||||
|
attachmentService.deleteByRef(AttachmentService.REF_DIARY, id);
|
||||||
|
mapper.deleteDiaryById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================== helpers
|
||||||
|
|
||||||
|
private void applySave(UiwsDiary d, DiarySaveDto dto) {
|
||||||
|
d.setTitle(dto.title());
|
||||||
|
d.setContent(dto.content());
|
||||||
|
d.setScheduleId(dto.scheduleId());
|
||||||
|
d.setDiaryDate(parseDateNullable(dto.diaryDate()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private DiaryDetailDto toDetail(UiwsDiary d) {
|
||||||
|
String writerNm = userNames(List.of(d.getWriterId())).getOrDefault(d.getWriterId(), d.getWriterId());
|
||||||
|
List<AttachmentDto> attachments = attachmentService.list(AttachmentService.REF_DIARY, d.getDiaryId());
|
||||||
|
return new DiaryDetailDto(d.getDiaryId(), d.getTitle(), writerNm,
|
||||||
|
d.getDiaryDate() != null ? d.getDiaryDate().toString() : null,
|
||||||
|
d.getScheduleId(), d.getContent(), attachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private UiwsDiary find(Long id) {
|
||||||
|
UiwsDiary d = mapper.findDiaryById(id);
|
||||||
|
if (d == null) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.DIARY_NOT_FOUND);
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> userNames(List<String> ids) {
|
||||||
|
List<String> clean = ids.stream().filter(s -> s != null && !s.isBlank()).distinct().toList();
|
||||||
|
if (clean.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
return mapper.findUserNames(clean).stream()
|
||||||
|
.collect(Collectors.toMap(r -> r.get("username"), r -> r.get("fullName"), (a, b) -> a));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int totalPages(long total, int size) {
|
||||||
|
return size <= 0 ? 0 : (int) ((total + size - 1) / size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalDate parseDateNullable(String s) {
|
||||||
|
if (s == null || s.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return LocalDate.parse(s);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "날짜 형식이 올바르지 않습니다(yyyy-MM-dd).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
package com.zioinfo.esn.uiws.schedule.service;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsApiException;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsErrorCode;
|
||||||
|
import com.zioinfo.esn.uiws.config.UiwsProperties;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 첨부파일 로컬 스토리지(UIWS 이식). 경로순회 방지(파일명 정규화 + UUID 저장명), 일자별 디렉터리 분리.
|
||||||
|
* 저장 결과로 상대경로(file_path)를 반환한다.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FileStorageService {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DAY = DateTimeFormatter.ofPattern("yyyy/MM/dd");
|
||||||
|
|
||||||
|
private final UiwsProperties properties;
|
||||||
|
|
||||||
|
public record StoredFile(String originalName, String relativePath, long size) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public StoredFile store(MultipartFile file) {
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.FILE_EMPTY);
|
||||||
|
}
|
||||||
|
String original = StringUtils.cleanPath(
|
||||||
|
file.getOriginalFilename() != null ? file.getOriginalFilename() : "file");
|
||||||
|
original = original.replace("\\", "_").replace("/", "_");
|
||||||
|
if (original.contains("..")) {
|
||||||
|
original = original.replace("..", "_");
|
||||||
|
}
|
||||||
|
String ext = "";
|
||||||
|
int dot = original.lastIndexOf('.');
|
||||||
|
if (dot >= 0) {
|
||||||
|
ext = original.substring(dot);
|
||||||
|
}
|
||||||
|
String subDir = LocalDate.now().format(DAY);
|
||||||
|
String storedName = UUID.randomUUID().toString().replace("-", "") + ext;
|
||||||
|
String relativePath = subDir + "/" + storedName;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Path root = Paths.get(properties.getUpload().getUploadDir()).toAbsolutePath().normalize();
|
||||||
|
Path target = root.resolve(relativePath).normalize();
|
||||||
|
if (!target.startsWith(root)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.FILE_STORAGE_ERROR, "허용되지 않은 저장 경로입니다.");
|
||||||
|
}
|
||||||
|
Files.createDirectories(target.getParent());
|
||||||
|
file.transferTo(target.toFile());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.FILE_STORAGE_ERROR);
|
||||||
|
}
|
||||||
|
return new StoredFile(original, relativePath, file.getSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path resolve(String relativePath) {
|
||||||
|
if (relativePath == null || relativePath.isBlank()) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.ATTACH_NOT_FOUND);
|
||||||
|
}
|
||||||
|
Path root = Paths.get(properties.getUpload().getUploadDir()).toAbsolutePath().normalize();
|
||||||
|
Path target = root.resolve(relativePath).normalize();
|
||||||
|
if (!target.startsWith(root)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.FILE_STORAGE_ERROR, "허용되지 않은 경로입니다.");
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(String relativePath) {
|
||||||
|
if (relativePath == null || relativePath.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Path root = Paths.get(properties.getUpload().getUploadDir()).toAbsolutePath().normalize();
|
||||||
|
Path target = root.resolve(relativePath).normalize();
|
||||||
|
if (target.startsWith(root)) {
|
||||||
|
Files.deleteIfExists(target);
|
||||||
|
}
|
||||||
|
} catch (IOException ignore) {
|
||||||
|
// 물리파일 삭제 실패는 무시(메타 일관성 우선)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,216 @@
|
|||||||
|
package com.zioinfo.esn.uiws.schedule.service;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsApiException;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsCurrentUser;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsDataScope;
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsErrorCode;
|
||||||
|
import com.zioinfo.esn.uiws.schedule.dto.ScheduleDtos.*;
|
||||||
|
import com.zioinfo.esn.uiws.schedule.mapper.ScheduleMapper;
|
||||||
|
import com.zioinfo.esn.uiws.schedule.model.UiwsSchedule;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.DayOfWeek;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.temporal.TemporalAdjusters;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일정(모듈 02) 서비스 — 원본 com.urp.uiws.schedule.service.ScheduleService 를 MyBatis 변환 이식.
|
||||||
|
* 달력(month/week/day)·전체목록·검색팝업·CRUD. 첨부 연계(attachmentIds).
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ScheduleService {
|
||||||
|
|
||||||
|
private static final String TYPE_PERSONAL = "PERSONAL";
|
||||||
|
private static final String TYPE_DEPT = "DEPT";
|
||||||
|
private static final DateTimeFormatter DT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
|
||||||
|
|
||||||
|
private final ScheduleMapper mapper;
|
||||||
|
private final AttachmentService attachmentService;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<ScheduleDto> calendar(String type, String view, LocalDate baseDate, String deptId) {
|
||||||
|
String t = normalizeType(type);
|
||||||
|
LocalDate base = (baseDate != null) ? baseDate : LocalDate.now();
|
||||||
|
LocalDate[] range = computeRange(view, base);
|
||||||
|
LocalDateTime from = range[0].atStartOfDay();
|
||||||
|
LocalDateTime to = range[1].atStartOfDay(); // 배타 상한
|
||||||
|
|
||||||
|
String ownerId = TYPE_PERSONAL.equals(t) ? UiwsCurrentUser.id() : null;
|
||||||
|
String deptFilter = TYPE_DEPT.equals(t) ? blankToNull(deptId) : null;
|
||||||
|
|
||||||
|
return mapper.findInRange(from, to, t, ownerId, deptFilter).stream().map(this::toDto).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public SchedulePage all(LocalDate fromDate, LocalDate toDate, String type, int page, int size) {
|
||||||
|
LocalDate to = (toDate != null) ? toDate : LocalDate.now();
|
||||||
|
LocalDate from = (fromDate != null) ? fromDate : to.minusMonths(1);
|
||||||
|
String t = (type == null || type.isBlank()) ? null : normalizeType(type);
|
||||||
|
UiwsDataScope.Scope scope = UiwsDataScope.current();
|
||||||
|
LocalDateTime f = from.atStartOfDay();
|
||||||
|
LocalDateTime tt = to.plusDays(1).atStartOfDay();
|
||||||
|
long total = mapper.countAll(f, tt, t, scope.all(), scope.ownerIds());
|
||||||
|
List<ScheduleDto> content = mapper.searchAll(f, tt, t, scope.all(), scope.ownerIds(), size, page * size)
|
||||||
|
.stream().map(this::toDto).toList();
|
||||||
|
return new SchedulePage(content, total, page, size, totalPages(total, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<ScheduleDto> search(String keyword) {
|
||||||
|
UiwsDataScope.Scope scope = UiwsDataScope.current();
|
||||||
|
return mapper.searchPopup(blankToNull(keyword), scope.all(), scope.ownerIds())
|
||||||
|
.stream().map(this::toDto).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ScheduleDto create(ScheduleSaveDto dto) {
|
||||||
|
String actor = UiwsCurrentUser.id();
|
||||||
|
UiwsSchedule s = new UiwsSchedule();
|
||||||
|
applySave(s, dto);
|
||||||
|
s.setOwnerId(actor);
|
||||||
|
s.setCreatedBy(actor);
|
||||||
|
s.setCreatedAt(LocalDateTime.now());
|
||||||
|
mapper.insertSchedule(s);
|
||||||
|
attachmentService.link(AttachmentService.REF_SCHEDULE, s.getScheduleId(), dto.attachmentIds());
|
||||||
|
return toDto(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public ScheduleDetailDto detail(Long id) {
|
||||||
|
return toDetail(find(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ScheduleDetailDto update(Long id, ScheduleSaveDto dto) {
|
||||||
|
String actor = UiwsCurrentUser.id();
|
||||||
|
UiwsSchedule s = find(id);
|
||||||
|
applySave(s, dto);
|
||||||
|
s.setUpdatedBy(actor);
|
||||||
|
s.setUpdatedAt(LocalDateTime.now());
|
||||||
|
mapper.updateSchedule(s);
|
||||||
|
attachmentService.link(AttachmentService.REF_SCHEDULE, s.getScheduleId(), dto.attachmentIds());
|
||||||
|
return toDetail(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(Long id) {
|
||||||
|
if (!mapper.existsScheduleById(id)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.SCHEDULE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
attachmentService.deleteByRef(AttachmentService.REF_SCHEDULE, id);
|
||||||
|
mapper.deleteScheduleById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================== helpers
|
||||||
|
|
||||||
|
private void applySave(UiwsSchedule s, ScheduleSaveDto dto) {
|
||||||
|
LocalDateTime start = parseDateTime(dto.startDt());
|
||||||
|
LocalDateTime end = parseDateTime(dto.endDt());
|
||||||
|
if (end.isBefore(start)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.SCHEDULE_DT_INVALID, "종료 일시가 시작 일시보다 빠릅니다.");
|
||||||
|
}
|
||||||
|
s.setScheType(normalizeType(dto.scheType()));
|
||||||
|
s.setTitle(dto.title());
|
||||||
|
s.setScheGubunCd(blankToNull(dto.scheGubunCd()));
|
||||||
|
s.setImportanceCd(blankToNull(dto.importanceCd()));
|
||||||
|
s.setStartDt(start);
|
||||||
|
s.setEndDt(end);
|
||||||
|
s.setContent(dto.content());
|
||||||
|
s.setDeptId(blankToNull(dto.deptId()));
|
||||||
|
s.setChargerId(blankToNull(dto.chargerId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScheduleDto toDto(UiwsSchedule s) {
|
||||||
|
return new ScheduleDto(s.getScheduleId(), s.getScheType(), s.getTitle(),
|
||||||
|
s.getScheGubunCd(), s.getImportanceCd(), fmt(s.getStartDt()), fmt(s.getEndDt()),
|
||||||
|
s.getOwnerId(), s.getDeptId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScheduleDetailDto toDetail(UiwsSchedule s) {
|
||||||
|
String chargerNm = null;
|
||||||
|
if (s.getChargerId() != null) {
|
||||||
|
chargerNm = userNames(List.of(s.getChargerId())).getOrDefault(s.getChargerId(), s.getChargerId());
|
||||||
|
}
|
||||||
|
List<AttachmentDto> attachments = attachmentService.list(AttachmentService.REF_SCHEDULE, s.getScheduleId());
|
||||||
|
return new ScheduleDetailDto(s.getScheduleId(), s.getScheType(), s.getTitle(),
|
||||||
|
s.getScheGubunCd(), s.getImportanceCd(), fmt(s.getStartDt()), fmt(s.getEndDt()),
|
||||||
|
s.getOwnerId(), s.getDeptId(), s.getContent(), s.getChargerId(), chargerNm, attachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private UiwsSchedule find(Long id) {
|
||||||
|
UiwsSchedule s = mapper.findScheduleById(id);
|
||||||
|
if (s == null) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.SCHEDULE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> userNames(List<String> ids) {
|
||||||
|
List<String> clean = ids.stream().filter(s -> s != null && !s.isBlank()).distinct().toList();
|
||||||
|
if (clean.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
return mapper.findUserNames(clean).stream()
|
||||||
|
.collect(Collectors.toMap(r -> r.get("username"), r -> r.get("fullName"), (a, b) -> a));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDate[] computeRange(String view, LocalDate base) {
|
||||||
|
String v = (view == null || view.isBlank()) ? "month" : view.trim().toLowerCase();
|
||||||
|
return switch (v) {
|
||||||
|
case "day" -> new LocalDate[]{base, base.plusDays(1)};
|
||||||
|
case "week" -> {
|
||||||
|
LocalDate start = base.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
|
||||||
|
yield new LocalDate[]{start, start.plusWeeks(1)};
|
||||||
|
}
|
||||||
|
case "month" -> {
|
||||||
|
LocalDate start = base.withDayOfMonth(1);
|
||||||
|
yield new LocalDate[]{start, start.plusMonths(1)};
|
||||||
|
}
|
||||||
|
default -> throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST,
|
||||||
|
"view 는 month/week/day 중 하나여야 합니다.");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeType(String type) {
|
||||||
|
String t = (type == null) ? "" : type.trim().toUpperCase();
|
||||||
|
if (!TYPE_PERSONAL.equals(t) && !TYPE_DEPT.equals(t)) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "type 은 PERSONAL/DEPT 중 하나여야 합니다.");
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int totalPages(long total, int size) {
|
||||||
|
return size <= 0 ? 0 : (int) ((total + size - 1) / size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String blankToNull(String s) {
|
||||||
|
return (s == null || s.isBlank()) ? null : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String fmt(LocalDateTime dt) {
|
||||||
|
return dt != null ? dt.format(DT) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalDateTime parseDateTime(String s) {
|
||||||
|
if (s == null || s.isBlank()) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "일시 값이 비어 있습니다.");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (s.length() <= 10) {
|
||||||
|
return LocalDate.parse(s).atStartOfDay();
|
||||||
|
}
|
||||||
|
return LocalDateTime.parse(s);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST,
|
||||||
|
"일시 형식이 올바르지 않습니다(yyyy-MM-ddTHH:mm:ss).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
package com.zioinfo.esn.uiws.stats.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.stats.dto.StatsDtos.PivotResponse;
|
||||||
|
import com.zioinfo.esn.uiws.stats.service.StatsService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UIWS 이식 — 업무통계(모듈 04, 동적 컬럼 피벗). 인증 필수(/api/stats).
|
||||||
|
* prefix /api/stats 는 ERP 기존 라우트와 미충돌(확인). 원본 엔드포인트 보존.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/stats")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class StatsController {
|
||||||
|
|
||||||
|
private final StatsService statsService;
|
||||||
|
|
||||||
|
@GetMapping("/personal-work")
|
||||||
|
public ApiResponse<PivotResponse> personalWork(
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromDate,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toDate,
|
||||||
|
@RequestParam(required = false) String deptId,
|
||||||
|
@RequestParam(required = false) String userId,
|
||||||
|
@RequestParam(required = false) Boolean showStatus,
|
||||||
|
@RequestParam(required = false) Boolean showType) {
|
||||||
|
return ApiResponse.ok(statsService.personalWork(fromDate, toDate, deptId, userId, showStatus, showType));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/company-work")
|
||||||
|
public ApiResponse<PivotResponse> companyWork(
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromDate,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toDate,
|
||||||
|
@RequestParam(required = false) String companyId,
|
||||||
|
@RequestParam(required = false) String userId,
|
||||||
|
@RequestParam(required = false) Boolean showType) {
|
||||||
|
return ApiResponse.ok(statsService.companyWork(fromDate, toDate, companyId, userId, showType));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package com.zioinfo.esn.uiws.stats.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 근무현황 통계(모듈 04) 동적 컬럼 피벗 DTO. 원본 com.urp.uiws.stats.dto.* 이식.
|
||||||
|
*/
|
||||||
|
public final class StatsDtos {
|
||||||
|
|
||||||
|
private StatsDtos() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 피벗 열 메타. 고정열 예 {key:"worker",label:"근무자"}; 동적열 예 {key:"company_본사",label:"본사"}. */
|
||||||
|
public record PivotColumn(String key, String label) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 동적 컬럼 피벗 응답. { fixedColumns, dynamicColumns, rows } */
|
||||||
|
public record PivotResponse(
|
||||||
|
List<PivotColumn> fixedColumns,
|
||||||
|
List<PivotColumn> dynamicColumns,
|
||||||
|
List<Map<String, Object>> rows
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
package com.zioinfo.esn.uiws.stats.mapper;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 근무현황 피벗 집계 매퍼. 원본 StatsRepository(JdbcTemplate 네이티브) → MyBatis 변환 이식.
|
||||||
|
*
|
||||||
|
* silo 정합(코어 TB_USER/TB_COMPANY/TB_CODE 미이식):
|
||||||
|
* - 근무자 라벨: esn_user (writer_id=username 라벨). 미존재 시 writer_id 노출(COALESCE).
|
||||||
|
* - 근무처(company) 라벨: company_id 직접 사용(거래처 테이블 미이식).
|
||||||
|
* - 근무상태/근무유형 라벨: 코드값 직접 사용(공통코드 미이식).
|
||||||
|
* 결과는 long-form(그룹키 + cnt). 서비스가 동적 컬럼으로 피벗한다.
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface StatsMapper {
|
||||||
|
|
||||||
|
/** 개인별: 행=근무자×근무상태×근무유형, 동적열 차원=근무처. 컬럼: worker, work_status, work_type, dyn, cnt. */
|
||||||
|
List<Map<String, Object>> personalWork(@Param("from") LocalDate from,
|
||||||
|
@Param("to") LocalDate to,
|
||||||
|
@Param("userId") String userId,
|
||||||
|
@Param("scopeAll") boolean scopeAll,
|
||||||
|
@Param("ownerIds") List<String> ownerIds);
|
||||||
|
|
||||||
|
/** 업체별: 행=근무처×근무유형, 동적열 차원=근무자. 컬럼: company, work_type, dyn, cnt. */
|
||||||
|
List<Map<String, Object>> companyWork(@Param("from") LocalDate from,
|
||||||
|
@Param("to") LocalDate to,
|
||||||
|
@Param("companyId") String companyId,
|
||||||
|
@Param("userId") String userId,
|
||||||
|
@Param("scopeAll") boolean scopeAll,
|
||||||
|
@Param("ownerIds") List<String> ownerIds);
|
||||||
|
}
|
||||||
@ -0,0 +1,165 @@
|
|||||||
|
package com.zioinfo.esn.uiws.stats.service;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.common.UiwsDataScope;
|
||||||
|
import com.zioinfo.esn.uiws.stats.dto.StatsDtos.PivotColumn;
|
||||||
|
import com.zioinfo.esn.uiws.stats.dto.StatsDtos.PivotResponse;
|
||||||
|
import com.zioinfo.esn.uiws.stats.mapper.StatsMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 근무현황 통계(모듈 04) — 동적 컬럼 피벗 서비스. 원본 com.urp.uiws.stats.service.StatsService 이식.
|
||||||
|
* long-form 집계를 wide-form 으로 피벗. showStatus/showType 토글. 기본 조회기간 = 최근 1주일.
|
||||||
|
* 데이터 스코프(ADMIN=전체/그 외=본인)는 UiwsDataScope 로 silo 판정.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class StatsService {
|
||||||
|
|
||||||
|
/** 행 식별 키 구분자(데이터에 등장하지 않는 제어문자). */
|
||||||
|
private static final char SEP = '';
|
||||||
|
|
||||||
|
private final StatsMapper mapper;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PivotResponse personalWork(LocalDate fromDate, LocalDate toDate,
|
||||||
|
String deptId, String userId,
|
||||||
|
Boolean showStatus, Boolean showType) {
|
||||||
|
LocalDate[] range = range(fromDate, toDate);
|
||||||
|
UiwsDataScope.Scope scope = UiwsDataScope.current();
|
||||||
|
// deptId 는 코어 dept 미이식으로 silo 에서 무시(파라미터 호환만 유지).
|
||||||
|
List<Map<String, Object>> agg = mapper.personalWork(
|
||||||
|
range[0], range[1], emptyToNull(userId), scope.all(), scope.ownerIds());
|
||||||
|
|
||||||
|
boolean status = !Boolean.FALSE.equals(showStatus);
|
||||||
|
boolean type = !Boolean.FALSE.equals(showType);
|
||||||
|
|
||||||
|
List<PivotColumn> fixed = new ArrayList<>();
|
||||||
|
fixed.add(new PivotColumn("worker", "근무자"));
|
||||||
|
if (status) {
|
||||||
|
fixed.add(new PivotColumn("workStatus", "근무상태"));
|
||||||
|
}
|
||||||
|
if (type) {
|
||||||
|
fixed.add(new PivotColumn("workType", "근무유형"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pivot(agg, fixed, "company",
|
||||||
|
row -> rowKey(row, status, type),
|
||||||
|
row -> {
|
||||||
|
Map<String, Object> base = new LinkedHashMap<>();
|
||||||
|
base.put("worker", str(row.get("worker")));
|
||||||
|
if (status) {
|
||||||
|
base.put("workStatus", str(row.get("work_status")));
|
||||||
|
}
|
||||||
|
if (type) {
|
||||||
|
base.put("workType", str(row.get("work_type")));
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PivotResponse companyWork(LocalDate fromDate, LocalDate toDate,
|
||||||
|
String companyId, String userId,
|
||||||
|
Boolean showType) {
|
||||||
|
LocalDate[] range = range(fromDate, toDate);
|
||||||
|
UiwsDataScope.Scope scope = UiwsDataScope.current();
|
||||||
|
List<Map<String, Object>> agg = mapper.companyWork(
|
||||||
|
range[0], range[1], emptyToNull(companyId), emptyToNull(userId), scope.all(), scope.ownerIds());
|
||||||
|
|
||||||
|
boolean type = !Boolean.FALSE.equals(showType);
|
||||||
|
|
||||||
|
List<PivotColumn> fixed = new ArrayList<>();
|
||||||
|
fixed.add(new PivotColumn("company", "근무처"));
|
||||||
|
if (type) {
|
||||||
|
fixed.add(new PivotColumn("workType", "근무유형"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pivot(agg, fixed, "worker",
|
||||||
|
row -> str(row.get("company")) + SEP + (type ? str(row.get("work_type")) : ""),
|
||||||
|
row -> {
|
||||||
|
Map<String, Object> base = new LinkedHashMap<>();
|
||||||
|
base.put("company", str(row.get("company")));
|
||||||
|
if (type) {
|
||||||
|
base.put("workType", str(row.get("work_type")));
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================== 피벗 공통
|
||||||
|
|
||||||
|
private interface RowKeyFn {
|
||||||
|
String key(Map<String, Object> row);
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface BaseFn {
|
||||||
|
Map<String, Object> base(Map<String, Object> row);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PivotResponse pivot(List<Map<String, Object>> agg, List<PivotColumn> fixed,
|
||||||
|
String dynPrefix, RowKeyFn keyFn, BaseFn baseFn) {
|
||||||
|
Map<String, PivotColumn> dynCols = new LinkedHashMap<>();
|
||||||
|
Map<String, Map<String, Object>> rowMap = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
for (Map<String, Object> r : agg) {
|
||||||
|
String dynLabel = str(r.get("dyn"));
|
||||||
|
String dynKey = dynPrefix + "_" + dynLabel;
|
||||||
|
dynCols.putIfAbsent(dynKey, new PivotColumn(dynKey, dynLabel));
|
||||||
|
|
||||||
|
String rk = keyFn.key(r);
|
||||||
|
Map<String, Object> row = rowMap.computeIfAbsent(rk, k -> baseFn.base(r));
|
||||||
|
long cnt = toLong(r.get("cnt"));
|
||||||
|
row.merge(dynKey, cnt, (a, b) -> toLong(a) + toLong(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PivotColumn> dynamic = new ArrayList<>(dynCols.values());
|
||||||
|
List<Map<String, Object>> rows = new ArrayList<>();
|
||||||
|
for (Map<String, Object> row : rowMap.values()) {
|
||||||
|
for (PivotColumn dc : dynamic) {
|
||||||
|
row.putIfAbsent(dc.key(), 0L);
|
||||||
|
}
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
return new PivotResponse(fixed, dynamic, rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String rowKey(Map<String, Object> row, boolean status, boolean type) {
|
||||||
|
StringBuilder sb = new StringBuilder(str(row.get("worker")));
|
||||||
|
if (status) {
|
||||||
|
sb.append(SEP).append(str(row.get("work_status")));
|
||||||
|
}
|
||||||
|
if (type) {
|
||||||
|
sb.append(SEP).append(str(row.get("work_type")));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDate[] range(LocalDate fromDate, LocalDate toDate) {
|
||||||
|
LocalDate to = (toDate != null) ? toDate : LocalDate.now();
|
||||||
|
LocalDate from = (fromDate != null) ? fromDate : to.minusWeeks(1);
|
||||||
|
return new LocalDate[]{from, to};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String emptyToNull(String s) {
|
||||||
|
return (s == null || s.isBlank()) ? null : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String str(Object o) {
|
||||||
|
return o == null ? "" : o.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long toLong(Object o) {
|
||||||
|
if (o == null) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
return (o instanceof Number n) ? n.longValue() : Long.parseLong(o.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CodeGrpDetailDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CodeGrpDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CodeGrpSaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CodeValueDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.CodeService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 2.7 코드(codes) — 그룹목록/상세/CRUD/드롭다운값. base: /api/system/codes */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/system/codes")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CodeController {
|
||||||
|
|
||||||
|
private final CodeService codeService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResponse<CodeGrpDto>> listGroups(
|
||||||
|
@RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(codeService.listGroups(keyword, page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 드롭다운 공용 코드값 조회(정적 경로 우선). */
|
||||||
|
@GetMapping("/group/{grpCd}/values")
|
||||||
|
public ApiResponse<List<CodeValueDto>> getValues(@PathVariable("grpCd") String grpCd) {
|
||||||
|
return ApiResponse.ok(codeService.getValues(grpCd));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{grpCd}")
|
||||||
|
public ApiResponse<CodeGrpDetailDto> getGroup(@PathVariable("grpCd") String grpCd) {
|
||||||
|
return ApiResponse.ok(codeService.getGroup(grpCd));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<CodeGrpDetailDto> create(@Valid @RequestBody CodeGrpSaveDto dto) {
|
||||||
|
return ApiResponse.ok(codeService.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{grpCd}")
|
||||||
|
public ApiResponse<CodeGrpDetailDto> update(@PathVariable("grpCd") String grpCd, @Valid @RequestBody CodeGrpSaveDto dto) {
|
||||||
|
return ApiResponse.ok(codeService.update(grpCd, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{grpCd}")
|
||||||
|
public ApiResponse<Void> delete(@PathVariable("grpCd") String grpCd) {
|
||||||
|
codeService.delete(grpCd);
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CompanyDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CompanySaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.IdsRequest;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.CompanyService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 2.4 거래처(companies) — 목록/검색팝업/CRUD/다중삭제. base: /api/system/companies */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/system/companies")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CompanyController {
|
||||||
|
|
||||||
|
private final CompanyService companyService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResponse<CompanyDto>> list(
|
||||||
|
@RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(companyService.list(keyword, page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/search")
|
||||||
|
public ApiResponse<List<CompanyDto>> search(@RequestParam(required = false) String keyword) {
|
||||||
|
return ApiResponse.ok(companyService.searchPopup(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<CompanyDto> create(@Valid @RequestBody CompanySaveDto dto) {
|
||||||
|
return ApiResponse.ok(companyService.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<CompanyDto> get(@PathVariable("id") String id) {
|
||||||
|
return ApiResponse.ok(companyService.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ApiResponse<CompanyDto> update(@PathVariable("id") String id, @Valid @RequestBody CompanySaveDto dto) {
|
||||||
|
return ApiResponse.ok(companyService.update(id, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping
|
||||||
|
public ApiResponse<Void> delete(@Valid @RequestBody IdsRequest req) {
|
||||||
|
companyService.delete(req.ids());
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.DeptDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.DeptSaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.DeptTreeDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.DeptService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 2.3 부서(depts) — 목록/검색팝업/트리/CRUD. base: /api/system/depts */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/system/depts")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DeptController {
|
||||||
|
|
||||||
|
private final DeptService deptService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResponse<DeptDto>> list(
|
||||||
|
@RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(deptService.list(keyword, page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/search")
|
||||||
|
public ApiResponse<List<DeptDto>> search(@RequestParam(required = false) String keyword) {
|
||||||
|
return ApiResponse.ok(deptService.searchPopup(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/tree")
|
||||||
|
public ApiResponse<List<DeptTreeDto>> tree() {
|
||||||
|
return ApiResponse.ok(deptService.tree());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<DeptDto> create(@Valid @RequestBody DeptSaveDto dto) {
|
||||||
|
return ApiResponse.ok(deptService.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<DeptDto> get(@PathVariable("id") String id) {
|
||||||
|
return ApiResponse.ok(deptService.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ApiResponse<DeptDto> update(@PathVariable("id") String id, @Valid @RequestBody DeptSaveDto dto) {
|
||||||
|
return ApiResponse.ok(deptService.update(id, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ApiResponse<Void> delete(@PathVariable("id") String id) {
|
||||||
|
deptService.delete(id);
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.DeptUserRoleDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.RoleIdsRequest;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.DeptRoleService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/** 2.2 부서권한(dept-role) — 부서 사용자+권한 조회 / 부여 / 삭제. base: /api/system/depts/{deptId}/roles */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/system/depts/{deptId}/roles")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DeptRoleController {
|
||||||
|
|
||||||
|
private final DeptRoleService deptRoleService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResponse<DeptUserRoleDto>> list(
|
||||||
|
@PathVariable("deptId") String deptId,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(deptRoleService.listDeptUsers(deptId, page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<Void> grant(@PathVariable("deptId") String deptId, @Valid @RequestBody RoleIdsRequest req) {
|
||||||
|
deptRoleService.grant(deptId, req.roleIds());
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping
|
||||||
|
public ApiResponse<Void> revoke(@PathVariable("deptId") String deptId, @Valid @RequestBody RoleIdsRequest req) {
|
||||||
|
deptRoleService.revoke(deptId, req.roleIds());
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.IdsRequest;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.MenuDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.MenuSaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.MenuService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 2.9 메뉴(menus) — 트리목록/검색/CRUD/다중삭제. base: /api/system/menus */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/system/menus")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MenuController {
|
||||||
|
|
||||||
|
private final MenuService menuService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResponse<MenuDto>> list(
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "100") int size) {
|
||||||
|
return ApiResponse.ok(menuService.list(page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/search")
|
||||||
|
public ApiResponse<List<MenuDto>> search(@RequestParam(required = false) String keyword) {
|
||||||
|
return ApiResponse.ok(menuService.searchPopup(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<MenuDto> create(@Valid @RequestBody MenuSaveDto dto) {
|
||||||
|
return ApiResponse.ok(menuService.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<MenuDto> get(@PathVariable("id") String id) {
|
||||||
|
return ApiResponse.ok(menuService.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ApiResponse<MenuDto> update(@PathVariable("id") String id, @Valid @RequestBody MenuSaveDto dto) {
|
||||||
|
return ApiResponse.ok(menuService.update(id, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping
|
||||||
|
public ApiResponse<Void> delete(@Valid @RequestBody IdsRequest req) {
|
||||||
|
menuService.delete(req.ids());
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.IdsRequest;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.ProgramDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.ProgramSaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.ProgramService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 2.8 프로그램(programs) — 목록/검색/CRUD/다중삭제. base: /api/system/programs */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/system/programs")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ProgramController {
|
||||||
|
|
||||||
|
private final ProgramService programService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResponse<ProgramDto>> list(
|
||||||
|
@RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(required = false) String programType,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(programService.list(keyword, programType, page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/search")
|
||||||
|
public ApiResponse<List<ProgramDto>> search(@RequestParam(required = false) String keyword) {
|
||||||
|
return ApiResponse.ok(programService.searchPopup(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<ProgramDto> create(@Valid @RequestBody ProgramSaveDto dto) {
|
||||||
|
return ApiResponse.ok(programService.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<ProgramDto> get(@PathVariable("id") String id) {
|
||||||
|
return ApiResponse.ok(programService.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ApiResponse<ProgramDto> update(@PathVariable("id") String id, @Valid @RequestBody ProgramSaveDto dto) {
|
||||||
|
return ApiResponse.ok(programService.update(id, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping
|
||||||
|
public ApiResponse<Void> delete(@Valid @RequestBody IdsRequest req) {
|
||||||
|
programService.delete(req.ids());
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PublicCompanyDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PublicDeptDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.PublicLookupService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 가입 화면(비인증) 공개 조회. base: /api/public/{depts,companies} */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/public")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PublicLookupController {
|
||||||
|
|
||||||
|
private final PublicLookupService publicLookupService;
|
||||||
|
|
||||||
|
@GetMapping("/depts")
|
||||||
|
public ApiResponse<List<PublicDeptDto>> depts() {
|
||||||
|
return ApiResponse.ok(publicLookupService.depts());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/companies")
|
||||||
|
public ApiResponse<List<PublicCompanyDto>> companies() {
|
||||||
|
return ApiResponse.ok(publicLookupService.companies());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.IdsRequest;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.RoleDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.RoleSaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.RoleService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/** 2.1 권한(roles) — 목록/등록/상세/수정/다중삭제. base: /api/system/roles */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/system/roles")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RoleController {
|
||||||
|
|
||||||
|
private final RoleService roleService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResponse<RoleDto>> list(
|
||||||
|
@RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(roleService.list(keyword, page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<RoleDto> create(@Valid @RequestBody RoleSaveDto dto) {
|
||||||
|
return ApiResponse.ok(roleService.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<RoleDto> get(@PathVariable("id") String id) {
|
||||||
|
return ApiResponse.ok(roleService.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ApiResponse<RoleDto> update(@PathVariable("id") String id, @Valid @RequestBody RoleSaveDto dto) {
|
||||||
|
return ApiResponse.ok(roleService.update(id, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping
|
||||||
|
public ApiResponse<Void> delete(@Valid @RequestBody IdsRequest req) {
|
||||||
|
roleService.delete(req.ids());
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.RoleDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.RoleMenuDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.RoleMenuSaveRequest;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.RoleMenuService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 2.5 메뉴생성(role-menus) — 권한목록 / 권한별 메뉴매핑 조회·저장. */
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RoleMenuController {
|
||||||
|
|
||||||
|
private final RoleMenuService roleMenuService;
|
||||||
|
|
||||||
|
@GetMapping("/api/system/role-menus")
|
||||||
|
public ApiResponse<PageResponse<RoleDto>> listRoles(
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(roleMenuService.listRoles(page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/api/system/roles/{roleId}/menus")
|
||||||
|
public ApiResponse<List<RoleMenuDto>> getRoleMenus(@PathVariable("roleId") String roleId) {
|
||||||
|
return ApiResponse.ok(roleMenuService.getRoleMenus(roleId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/api/system/roles/{roleId}/menus")
|
||||||
|
public ApiResponse<Void> saveRoleMenus(@PathVariable("roleId") String roleId, @Valid @RequestBody RoleMenuSaveRequest req) {
|
||||||
|
roleMenuService.saveRoleMenus(roleId, req.menus());
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.controller;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.common.ApiResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.CheckIdResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.IdsRequest;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.PageResponse;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.UserDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.dto.UserSaveDto;
|
||||||
|
import com.zioinfo.esn.uiws.system.service.UserService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 2.6 사용자(users) — 목록/검색/중복ID확인/CRUD/다중삭제/비번초기화/잠금해제/승인. base: /api/system/users */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/system/users")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResponse<UserDto>> list(
|
||||||
|
@RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(required = false) String deptId,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
return ApiResponse.ok(userService.list(keyword, deptId, page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/search")
|
||||||
|
public ApiResponse<List<UserDto>> search(
|
||||||
|
@RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(required = false) String deptId) {
|
||||||
|
return ApiResponse.ok(userService.searchPopup(keyword, deptId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/check-id")
|
||||||
|
public ApiResponse<CheckIdResponse> checkId(@RequestParam String userId) {
|
||||||
|
return ApiResponse.ok(userService.checkId(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<UserDto> create(@Valid @RequestBody UserSaveDto dto) {
|
||||||
|
return ApiResponse.ok(userService.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<UserDto> get(@PathVariable("id") String id) {
|
||||||
|
return ApiResponse.ok(userService.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ApiResponse<UserDto> update(@PathVariable("id") String id, @Valid @RequestBody UserSaveDto dto) {
|
||||||
|
return ApiResponse.ok(userService.update(id, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping
|
||||||
|
public ApiResponse<Void> delete(@Valid @RequestBody IdsRequest req) {
|
||||||
|
userService.delete(req.ids());
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/reset-pw")
|
||||||
|
public ApiResponse<Void> resetPassword(@PathVariable("id") String id) {
|
||||||
|
userService.resetPassword(id);
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/unlock")
|
||||||
|
public ApiResponse<Void> unlock(@PathVariable("id") String id) {
|
||||||
|
userService.unlock(id);
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/approve")
|
||||||
|
public ApiResponse<Void> approve(@PathVariable("id") String id) {
|
||||||
|
userService.approve(id);
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/revoke-approval")
|
||||||
|
public ApiResponse<Void> revokeApproval(@PathVariable("id") String id) {
|
||||||
|
userService.revokeApproval(id);
|
||||||
|
return ApiResponse.ok(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** { available: boolean } */
|
||||||
|
public record CheckIdResponse(boolean available) {}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** { grpCd, grpNm, useYn, values: CodeValueDto[] } */
|
||||||
|
public record CodeGrpDetailDto(String grpCd, String grpNm, String useYn, List<CodeValueDto> values) {}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** { grpCd, grpNm, useYn } */
|
||||||
|
public record CodeGrpDto(String grpCd, String grpNm, String useYn) {}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** { grpCd, grpNm, useYn, values: CodeValueDto[] } */
|
||||||
|
public record CodeGrpSaveDto(
|
||||||
|
@NotBlank(message = "grpCd는 필수입니다.") String grpCd,
|
||||||
|
@NotBlank(message = "grpNm은 필수입니다.") String grpNm,
|
||||||
|
@NotBlank(message = "useYn은 필수입니다.") String useYn,
|
||||||
|
List<CodeValueDto> values) {}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** { grpCd, codeVal, codeNm, sortOrd, useYn } */
|
||||||
|
public record CodeValueDto(String grpCd, String codeVal, String codeNm, Integer sortOrd, String useYn) {}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** { companyId, companyNm, bizNo, useYn } */
|
||||||
|
public record CompanyDto(String companyId, String companyNm, String bizNo, String useYn) {}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/** { companyId, companyNm, bizNo, useYn } */
|
||||||
|
public record CompanySaveDto(
|
||||||
|
String companyId,
|
||||||
|
@NotBlank(message = "companyNm은 필수입니다.") String companyNm,
|
||||||
|
String bizNo,
|
||||||
|
@NotBlank(message = "useYn은 필수입니다.") String useYn) {}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** { deptId, deptNm, parentDeptId, sortOrd, useYn } */
|
||||||
|
public record DeptDto(String deptId, String deptNm, String parentDeptId, Integer sortOrd, String useYn) {}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/** { deptId, deptNm, parentDeptId, sortOrd, useYn } */
|
||||||
|
public record DeptSaveDto(
|
||||||
|
String deptId,
|
||||||
|
@NotBlank(message = "deptNm은 필수입니다.") String deptNm,
|
||||||
|
String parentDeptId,
|
||||||
|
Integer sortOrd,
|
||||||
|
@NotBlank(message = "useYn은 필수입니다.") String useYn) {}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 부서 계층 트리 노드. children 은 sortOrd→deptId 순. */
|
||||||
|
public record DeptTreeDto(
|
||||||
|
String deptId, String deptNm, String parentDeptId,
|
||||||
|
Integer sortOrd, String useYn, List<DeptTreeDto> children) {}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** { userId, userNm, roleIds } — 부서 사용자별 부여 권한. */
|
||||||
|
public record DeptUserRoleDto(String userId, String userNm, List<String> roleIds) {}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 다중삭제 공통 본문: { ids: string[] } */
|
||||||
|
public record IdsRequest(@NotEmpty(message = "ids는 필수입니다.") List<String> ids) {}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** { menuId, menuNm, parentMenuId, programId, menuUrl, sortOrd, useYn } */
|
||||||
|
public record MenuDto(
|
||||||
|
String menuId, String menuNm, String parentMenuId, String programId,
|
||||||
|
String menuUrl, Integer sortOrd, String useYn) {}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/** { menuId, menuNm, parentMenuId, programId, menuUrl, sortOrd, useYn } */
|
||||||
|
public record MenuSaveDto(
|
||||||
|
String menuId,
|
||||||
|
@NotBlank(message = "menuNm은 필수입니다.") String menuNm,
|
||||||
|
String parentMenuId, String programId, String menuUrl, Integer sortOrd,
|
||||||
|
@NotBlank(message = "useYn은 필수입니다.") String useYn) {}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UIWS system 이식용 페이지 응답 봉투. 원본 com.urp.uiws.common.response.PageResponse 대체.
|
||||||
|
* MyBatis 기반(Spring Data Page 부재)이므로 content + total + page + size 를 직접 담는다.
|
||||||
|
*/
|
||||||
|
public record PageResponse<T>(
|
||||||
|
List<T> content,
|
||||||
|
long total,
|
||||||
|
int page,
|
||||||
|
int size
|
||||||
|
) {
|
||||||
|
public static <T> PageResponse<T> of(List<T> content, long total, int page, int size) {
|
||||||
|
return new PageResponse<>(content, total, page, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** { programId, programNm, programType, programUrl, category, useYn } */
|
||||||
|
public record ProgramDto(
|
||||||
|
String programId, String programNm, String programType,
|
||||||
|
String programUrl, String category, String useYn) {}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/** { programId, programNm, programType, programUrl, category, useYn } */
|
||||||
|
public record ProgramSaveDto(
|
||||||
|
@NotBlank(message = "programId는 필수입니다.") String programId,
|
||||||
|
@NotBlank(message = "programNm은 필수입니다.") String programNm,
|
||||||
|
@NotBlank(message = "programType은 필수입니다.") String programType,
|
||||||
|
String programUrl, String category,
|
||||||
|
@NotBlank(message = "useYn은 필수입니다.") String useYn) {}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** 가입 화면 공개 조회용 거래처: { companyId, companyNm } */
|
||||||
|
public record PublicCompanyDto(String companyId, String companyNm) {}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** 가입 화면 공개 조회용 부서: { deptId, deptNm } */
|
||||||
|
public record PublicDeptDto(String deptId, String deptNm) {}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** { roleId, roleNm, roleDesc, useYn } */
|
||||||
|
public record RoleDto(String roleId, String roleNm, String roleDesc, String useYn) {}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 부서권한 부여/삭제 본문: { roleIds: string[] } */
|
||||||
|
public record RoleIdsRequest(@NotEmpty(message = "roleIds는 필수입니다.") List<String> roleIds) {}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** { menuId, menuNm, readYn, writeYn } */
|
||||||
|
public record RoleMenuDto(String menuId, String menuNm, String readYn, String writeYn) {}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 권한별 메뉴 매핑 저장 본문: { menus: RoleMenuDto[] } */
|
||||||
|
public record RoleMenuSaveRequest(List<RoleMenuDto> menus) {}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/** { roleId, roleNm, roleDesc, useYn } */
|
||||||
|
public record RoleSaveDto(
|
||||||
|
String roleId,
|
||||||
|
@NotBlank(message = "roleNm은 필수입니다.") String roleNm,
|
||||||
|
String roleDesc,
|
||||||
|
@NotBlank(message = "useYn은 필수입니다.") String useYn) {}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
/** 사용자 조회 DTO(비밀번호 제외 — 절대 노출 금지). */
|
||||||
|
public record UserDto(
|
||||||
|
String userId, String userNm, String email, String gradeCd,
|
||||||
|
String deptId, String deptNm, String companyId, String companyNm,
|
||||||
|
String roleCd, String naverworksId, String lockYn, String useYn, String approvalYn) {}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/** roleCd = USER/MANAGER/ADMIN (null → USER). 시스템관리(ADMIN 전용)에서만 설정. */
|
||||||
|
public record UserSaveDto(
|
||||||
|
@NotBlank(message = "userId는 필수입니다.") String userId,
|
||||||
|
@NotBlank(message = "userNm은 필수입니다.") String userNm,
|
||||||
|
String password,
|
||||||
|
@NotBlank(message = "email은 필수입니다.") @Email(message = "email 형식이 올바르지 않습니다.") String email,
|
||||||
|
String gradeCd, String deptId, String companyId, String roleCd, String naverworksId,
|
||||||
|
@NotBlank(message = "useYn은 필수입니다.") String useYn) {}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.system.model.*;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 공통코드 그룹/값(tb_uiws_code_grp, tb_uiws_code) 매퍼. 원본 CodeGrpRepository/CodeRepository 변환. */
|
||||||
|
@Mapper
|
||||||
|
public interface CodeMapper {
|
||||||
|
List<SysCodeGrp> searchGroups(@Param("keyword") String keyword,
|
||||||
|
@Param("offset") int offset, @Param("size") int size);
|
||||||
|
long countGroups(@Param("keyword") String keyword);
|
||||||
|
SysCodeGrp findGroupById(@Param("grpCd") String grpCd);
|
||||||
|
boolean groupExists(@Param("grpCd") String grpCd);
|
||||||
|
int insertGroup(SysCodeGrp grp);
|
||||||
|
int updateGroup(SysCodeGrp grp);
|
||||||
|
int deleteGroup(@Param("grpCd") String grpCd);
|
||||||
|
List<SysCode> findValuesByGrp(@Param("grpCd") String grpCd);
|
||||||
|
List<SysCode> findActiveValuesByGrp(@Param("grpCd") String grpCd);
|
||||||
|
int insertValue(SysCode code);
|
||||||
|
int deleteValuesByGrp(@Param("grpCd") String grpCd);
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.system.model.*;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 거래처(tb_uiws_company) 매퍼. 원본 CompanyRepository 변환. */
|
||||||
|
@Mapper
|
||||||
|
public interface CompanyMapper {
|
||||||
|
List<SysCompany> search(@Param("keyword") String keyword,
|
||||||
|
@Param("offset") int offset, @Param("size") int size);
|
||||||
|
long countSearch(@Param("keyword") String keyword);
|
||||||
|
List<SysCompany> searchActive(@Param("keyword") String keyword);
|
||||||
|
List<SysCompany> findActiveOrdered();
|
||||||
|
List<SysCompany> findAll();
|
||||||
|
SysCompany findById(@Param("companyId") String companyId);
|
||||||
|
boolean existsById(@Param("companyId") String companyId);
|
||||||
|
int insert(SysCompany company);
|
||||||
|
int update(SysCompany company);
|
||||||
|
int deleteById(@Param("companyId") String companyId);
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.system.model.*;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 부서(tb_uiws_dept) 매퍼. 원본 DeptRepository 변환. */
|
||||||
|
@Mapper
|
||||||
|
public interface DeptMapper {
|
||||||
|
List<SysDept> search(@Param("keyword") String keyword,
|
||||||
|
@Param("offset") int offset, @Param("size") int size);
|
||||||
|
long countSearch(@Param("keyword") String keyword);
|
||||||
|
List<SysDept> searchActive(@Param("keyword") String keyword);
|
||||||
|
List<SysDept> findActiveOrdered();
|
||||||
|
List<SysDept> findAll();
|
||||||
|
SysDept findById(@Param("deptId") String deptId);
|
||||||
|
boolean existsById(@Param("deptId") String deptId);
|
||||||
|
boolean existsByParentDeptId(@Param("parentDeptId") String parentDeptId);
|
||||||
|
int insert(SysDept dept);
|
||||||
|
int update(SysDept dept);
|
||||||
|
int deleteById(@Param("deptId") String deptId);
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.system.model.*;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 부서-권한 매핑(tb_uiws_dept_role) 매퍼. 원본 DeptRoleRepository 변환. */
|
||||||
|
@Mapper
|
||||||
|
public interface DeptRoleMapper {
|
||||||
|
List<String> findRoleIdsByDeptId(@Param("deptId") String deptId);
|
||||||
|
boolean exists(@Param("deptId") String deptId, @Param("roleId") String roleId);
|
||||||
|
boolean existsByRoleId(@Param("roleId") String roleId);
|
||||||
|
int insert(@Param("deptId") String deptId, @Param("roleId") String roleId, @Param("actor") String actor);
|
||||||
|
int delete(@Param("deptId") String deptId, @Param("roleId") String roleId);
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.system.model.*;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 메뉴(tb_uiws_menu) 매퍼. 원본 SysMenuRepository 변환. */
|
||||||
|
@Mapper
|
||||||
|
public interface MenuMapper {
|
||||||
|
List<SysMenu> searchAll(@Param("offset") int offset, @Param("size") int size);
|
||||||
|
long countAll();
|
||||||
|
List<SysMenu> search(@Param("keyword") String keyword);
|
||||||
|
List<SysMenu> findAllOrdered();
|
||||||
|
SysMenu findById(@Param("menuId") String menuId);
|
||||||
|
boolean existsById(@Param("menuId") String menuId);
|
||||||
|
boolean existsByParentMenuId(@Param("parentMenuId") String parentMenuId);
|
||||||
|
boolean existsByProgramId(@Param("programId") String programId);
|
||||||
|
int insert(SysMenu menu);
|
||||||
|
int update(SysMenu menu);
|
||||||
|
int deleteById(@Param("menuId") String menuId);
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.system.model.*;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 프로그램(tb_uiws_program) 매퍼. 원본 ProgramRepository 변환. */
|
||||||
|
@Mapper
|
||||||
|
public interface ProgramMapper {
|
||||||
|
List<SysProgram> search(@Param("keyword") String keyword, @Param("programType") String programType,
|
||||||
|
@Param("offset") int offset, @Param("size") int size);
|
||||||
|
long countSearch(@Param("keyword") String keyword, @Param("programType") String programType);
|
||||||
|
List<SysProgram> searchActive(@Param("keyword") String keyword);
|
||||||
|
SysProgram findById(@Param("programId") String programId);
|
||||||
|
boolean existsById(@Param("programId") String programId);
|
||||||
|
int insert(SysProgram program);
|
||||||
|
int update(SysProgram program);
|
||||||
|
int deleteById(@Param("programId") String programId);
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.system.model.*;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 권한(tb_uiws_role) 매퍼. 원본 RoleRepository 변환. */
|
||||||
|
@Mapper
|
||||||
|
public interface RoleMapper {
|
||||||
|
List<SysRole> search(@Param("keyword") String keyword,
|
||||||
|
@Param("offset") int offset, @Param("size") int size);
|
||||||
|
long countSearch(@Param("keyword") String keyword);
|
||||||
|
SysRole findById(@Param("roleId") String roleId);
|
||||||
|
boolean existsById(@Param("roleId") String roleId);
|
||||||
|
int insert(SysRole role);
|
||||||
|
int update(SysRole role);
|
||||||
|
int deleteById(@Param("roleId") String roleId);
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.system.model.*;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 권한-메뉴 매핑(tb_uiws_role_menu) 매퍼. 원본 RoleMenuRepository 변환. */
|
||||||
|
@Mapper
|
||||||
|
public interface RoleMenuMapper {
|
||||||
|
List<SysRoleMenu> findByRoleId(@Param("roleId") String roleId);
|
||||||
|
boolean existsByMenuId(@Param("menuId") String menuId);
|
||||||
|
int insert(SysRoleMenu rm);
|
||||||
|
int deleteByRoleId(@Param("roleId") String roleId);
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.mapper;
|
||||||
|
|
||||||
|
import com.zioinfo.esn.uiws.system.model.*;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 업무 사용자(tb_uiws_sys_user) 매퍼. 원본 SysUserRepository 변환. */
|
||||||
|
@Mapper
|
||||||
|
public interface SysUserMapper {
|
||||||
|
List<SysUser> search(@Param("keyword") String keyword, @Param("deptId") String deptId,
|
||||||
|
@Param("offset") int offset, @Param("size") int size);
|
||||||
|
long countSearch(@Param("keyword") String keyword, @Param("deptId") String deptId);
|
||||||
|
List<SysUser> searchActive(@Param("keyword") String keyword, @Param("deptId") String deptId);
|
||||||
|
List<SysUser> findByDeptId(@Param("deptId") String deptId);
|
||||||
|
SysUser findById(@Param("userId") String userId);
|
||||||
|
boolean existsById(@Param("userId") String userId);
|
||||||
|
int insert(SysUser user);
|
||||||
|
int update(SysUser user);
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 공통코드 값 (tb_uiws_code, 복합 PK grp_cd+code_val). 원본 com.urp.uiws.domain.Code 이식. */
|
||||||
|
@Data
|
||||||
|
public class SysCode {
|
||||||
|
private String grpCd;
|
||||||
|
private String codeVal;
|
||||||
|
private String codeNm;
|
||||||
|
private Integer sortOrd;
|
||||||
|
private String useYn;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 공통코드 그룹 (tb_uiws_code_grp). 원본 com.urp.uiws.domain.CodeGrp 이식. */
|
||||||
|
@Data
|
||||||
|
public class SysCodeGrp {
|
||||||
|
private String grpCd;
|
||||||
|
private String grpNm;
|
||||||
|
private String useYn;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 거래처(=근무처) (tb_uiws_company). 원본 com.urp.uiws.domain.Company 이식. */
|
||||||
|
@Data
|
||||||
|
public class SysCompany {
|
||||||
|
private String companyId;
|
||||||
|
private String companyNm;
|
||||||
|
private String bizNo;
|
||||||
|
private String useYn;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 부서 (tb_uiws_dept). 원본 com.urp.uiws.domain.Dept 이식. */
|
||||||
|
@Data
|
||||||
|
public class SysDept {
|
||||||
|
private String deptId;
|
||||||
|
private String deptNm;
|
||||||
|
private String parentDeptId;
|
||||||
|
private Integer sortOrd;
|
||||||
|
private String useYn;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 메뉴 (tb_uiws_menu, 2-depth 자기참조). 원본 com.urp.uiws.domain.Menu 이식. */
|
||||||
|
@Data
|
||||||
|
public class SysMenu {
|
||||||
|
private String menuId;
|
||||||
|
private String menuNm;
|
||||||
|
private String parentMenuId;
|
||||||
|
private String programId;
|
||||||
|
private String menuUrl;
|
||||||
|
private Integer sortOrd;
|
||||||
|
private String useYn;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 프로그램(화면) (tb_uiws_program). 원본 com.urp.uiws.domain.Program 이식. */
|
||||||
|
@Data
|
||||||
|
public class SysProgram {
|
||||||
|
private String programId;
|
||||||
|
private String programNm;
|
||||||
|
private String programType; // FORM | POPUP
|
||||||
|
private String programUrl;
|
||||||
|
private String category;
|
||||||
|
private String useYn;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.zioinfo.esn.uiws.system.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** 권한(역할) (tb_uiws_role). 원본 com.urp.uiws.domain.Role 이식. */
|
||||||
|
@Data
|
||||||
|
public class SysRole {
|
||||||
|
private String roleId;
|
||||||
|
private String roleNm;
|
||||||
|
private String roleDesc;
|
||||||
|
private String useYn;
|
||||||
|
private String createdBy;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String updatedBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user