출장등록 진행중 -> 화면 등록 완료

리스트 진행
This commit is contained in:
hehihoho3@gmail.com 2025-04-08 10:31:37 +09:00
parent b4b54de6d2
commit e572834961
32 changed files with 1184 additions and 893 deletions

View File

@ -37,6 +37,11 @@ public class RestResponse {
this.msg = msg;
this.data = data;
}
@Builder
public RestResponse(HttpStatus status, String msg) {
this.status = status;
this.msg = msg;
}
}

View File

@ -18,7 +18,7 @@ public class ItnCommuteVO implements Serializable {
private Integer commuteGroupId; // 그룹 아이디
private String uniqId;
private String userName; // 이름
private String userRank; // 직위
private String rankCd; // 직위
private String category; // 구분
private String workDt; // 근무일자
private String startTime; // 출근시간

View File

@ -109,7 +109,7 @@ public class CommuteServiceImpl implements CommuteService {
if( matchedUser != null ){
t.setUsrid(matchedUser.getUserName());
t.setPstn(matchedUser.getUserRank());
t.setPstn(matchedUser.getRankCd());
t.setUniqId(matchedUser.getUniqId());
}

View File

@ -0,0 +1,19 @@
package com.itn.admin.itn.bizTrip.mapper;
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripApprovalVO;
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripMemberVO;
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface BizTripMapper {
void insertBizTrip(BizTripVO trip);
void insertTripMember(BizTripMemberVO member);
void insertApprovalLine(BizTripApprovalVO approval);
List<BizTripVO> selectTripList();
}

View File

@ -0,0 +1,29 @@
package com.itn.admin.itn.bizTrip.mapper.domain;
import com.itn.admin.cmn.vo.CmnVO;
import lombok.*;
import lombok.experimental.SuperBuilder;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
@Getter
@Setter
@ToString
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class BizTripApprovalVO extends CmnVO {
private Integer id;
private Integer tripId; // 출장 ID (FK)
private String approverId; // 결재자 uniq_id
private Integer orderNo; // 결재 순서
private String approveStatus; // 결재 상태 (WAIT, APPROVED, REJECTED)
private LocalDateTime approveDt; // 결재 일시
private String comment; // 결재 의견
}

View File

@ -0,0 +1,25 @@
package com.itn.admin.itn.bizTrip.mapper.domain;
import com.itn.admin.cmn.vo.CmnVO;
import lombok.*;
import lombok.experimental.SuperBuilder;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalTime;
@Getter
@Setter
@ToString
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class BizTripMemberVO extends CmnVO {
private Integer id;
private Integer tripId; // 출장 ID (FK)
private String uniqId; // 유저 고유 ID (FK)
private String role; // 역할 (기안자: 0, 동행자: 1 )
}

View File

@ -0,0 +1,17 @@
package com.itn.admin.itn.bizTrip.mapper.domain;
import lombok.*;
import java.util.List;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BizTripRequestDTO {
private BizTripVO tripInfo;
private List<BizTripMemberVO> tripMembers;
private List<BizTripApprovalVO> approvalLines;
}

View File

@ -0,0 +1,33 @@
package com.itn.admin.itn.bizTrip.mapper.domain;
import com.itn.admin.cmn.vo.CmnVO;
import lombok.*;
import lombok.experimental.SuperBuilder;
import java.time.LocalDate;
import java.time.LocalTime;
@Getter
@Setter
@ToString
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class BizTripVO extends CmnVO {
private Integer tripId; // 출장 고유 ID
private String tripTypeCd; // 출장 구분 (TRIP_TYPE 공통코드)
private String locationCd; // 출장지 (TRIP_LOCATION 공통코드)
private String locationTxt; // 출장지 (TRIP_LOCATION 공통코드)
private String purpose; // 출장 목적
private String moveCd; // 이동 수단 (TRIP_MOVE 공통코드)
private LocalDate tripDt; // 출장일자
private LocalTime startTime; // 출장 시작시간
private LocalTime endTime; // 출장 종료시간
private String status; // 결재 상태 (ING, DONE )
private String latestApproveStatus; // 최신 결재 상태
}

View File

@ -0,0 +1,13 @@
package com.itn.admin.itn.bizTrip.service;
import com.itn.admin.cmn.msg.RestResponse;
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripRequestDTO;
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripVO;
import java.util.List;
public interface BizTripService {
RestResponse register(BizTripRequestDTO dto);
List<BizTripVO> selectTripList();
}

View File

@ -0,0 +1,60 @@
package com.itn.admin.itn.bizTrip.service.impl;
import com.itn.admin.cmn.msg.RestResponse;
import com.itn.admin.itn.bizTrip.mapper.BizTripMapper;
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripApprovalVO;
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripMemberVO;
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripRequestDTO;
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripVO;
import com.itn.admin.itn.bizTrip.service.BizTripService;
import com.itn.admin.itn.code.mapper.CodeMapper;
import com.itn.admin.itn.code.mapper.domain.CodeVO;
import com.itn.admin.itn.code.server.CodeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class BizTripServiceImpl implements BizTripService {
@Autowired
private BizTripMapper bizTripMapper;
@Override
public RestResponse register(BizTripRequestDTO dto) {
// 1. BizTripVO insert (자동 생성된 tripId 얻기)
BizTripVO trip = dto.getTripInfo();
bizTripMapper.insertBizTrip(trip); // insert trip.tripId에 PK 자동 세팅됨
Integer tripId = trip.getTripId();
// 2. 출장 참여 인원 등록
List<BizTripMemberVO> memberList = dto.getTripMembers();
if (memberList != null && !memberList.isEmpty()) {
for (BizTripMemberVO member : memberList) {
member.setTripId(tripId);
bizTripMapper.insertTripMember(member);
}
}
// 3. 결재 라인 등록
List<BizTripApprovalVO> approvalList = dto.getApprovalLines();
if (approvalList != null && !approvalList.isEmpty()) {
for (BizTripApprovalVO approval : approvalList) {
approval.setTripId(tripId);
bizTripMapper.insertApprovalLine(approval);
}
}
return new RestResponse(HttpStatus.OK, "등록되었습니다");
}
@Override
public List<BizTripVO> selectTripList() {
return bizTripMapper.selectTripList();
}
}

View File

@ -1,42 +1,42 @@
package com.itn.admin.itn.trip.web;
package com.itn.admin.itn.bizTrip.web;
import com.itn.admin.cmn.config.CustomUserDetails;
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripVO;
import com.itn.admin.itn.bizTrip.service.BizTripService;
import com.itn.admin.itn.user.mapper.domain.UserVO;
import com.itn.admin.itn.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import java.util.Map;
import java.util.List;
@Slf4j
@Controller
public class TripController {
public class BizTripController {
@Autowired
private UserService userService;
private BizTripService bizTripService;
@GetMapping("/itn/trip/reg")
public String list(@ModelAttribute("userVO") UserVO userVO
,@AuthenticationPrincipal CustomUserDetails loginUser
@GetMapping("/itn/bizTrip/reg")
public String list(@AuthenticationPrincipal CustomUserDetails loginUser
,Model model
) {
log.info(" + loginUser :: [{}]", loginUser.getUser());
/*
Map<String, Object> resultMap = userService.getList(userVO);
model.addAttribute("list", resultMap.get("resultList"));
*/
model.addAttribute("loginUser", loginUser.getUser());
return "itn/trip/reg";
return "itn/bizTrip/reg";
}
@GetMapping("/itn/bizTrip/list")
public String bizTripList(Model model) {
List<BizTripVO> list = bizTripService.selectTripList();
model.addAttribute("list", list);
return "itn/bizTrip/list"; // Thymeleaf HTML 파일 경로
}
}

View File

@ -0,0 +1,37 @@
package com.itn.admin.itn.bizTrip.web;
import com.itn.admin.cmn.config.CustomUserDetails;
import com.itn.admin.cmn.msg.RestResponse;
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripRequestDTO;
import com.itn.admin.itn.bizTrip.service.BizTripService;
import com.itn.admin.itn.code.mapper.domain.CodeDetailVO;
import com.itn.admin.itn.user.mapper.domain.UserVO;
import com.itn.admin.itn.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
public class BizTripRestController {
@Autowired
private BizTripService bizTripService;
@PostMapping("/api/bizTrip/register")
public ResponseEntity<RestResponse> registerBizTrip(@RequestBody BizTripRequestDTO dto
, @AuthenticationPrincipal CustomUserDetails loginUser) {
dto.getTripInfo().setFrstRegisterId(loginUser.getUser().getUniqId());
log.info("dto: [{}]", dto);
// bizTripService.register(dto);
return ResponseEntity.ok().body(bizTripService.register(dto));
}
}

View File

@ -41,6 +41,10 @@ public interface UserMapper {
void changepassword(UserVO userVO);
List<UserVO> selectUsersByNameAndRank(String userName);
@Select("SELECT * FROM users WHERE user_name LIKE CONCAT('%', #{userName}, '%')")
List<UserVO> findByUniqUserName(String userName);
}

View File

@ -21,7 +21,7 @@ public class UserVO extends CmnVO {
private String userId;
private String password;
private String userName;
private String userRank;
private String rankCd;
private String mobilePhone;
private String gwId;
private String biostarId;

View File

@ -26,4 +26,5 @@ public interface UserService {
RestResponse changepassword(UserVO userVO);
RestResponse findByUniqUserName(String userName);
RestResponse findByUniqApprovalUser(String userName);
}

View File

@ -107,6 +107,27 @@ public class UserServiceImpl implements UserService {
users.forEach(user -> {
String deptNm = tCodeUtils.getCodeName("DEPT", user.getDeptCd());
user.setDeptNm(deptNm); // UserVO에 deptCdName 필드 있어야
String rankNm = tCodeUtils.getCodeName("RANK", user.getRankCd());
user.setDeptNm(rankNm); // UserVO에 deptCdName 필드 있어야
});
return RestResponse.builder()
.status(HttpStatus.OK) // 200 성공
.data(users)
// .msg("수정되었습니다.")
.build();
} @Override
public RestResponse findByUniqApprovalUser(String userName) {
List<UserVO> users = userMapper.selectUsersByNameAndRank(userName);
// 코드 이름을 붙여주는 처리 (가공)
users.forEach(user -> {
String deptNm = tCodeUtils.getCodeName("DEPT", user.getDeptCd());
user.setDeptNm(deptNm); // UserVO에 deptCdName 필드 있어야
String rankNm = tCodeUtils.getCodeName("RANK", user.getRankCd());
user.setDeptNm(rankNm); // UserVO에 deptCdName 필드 있어야
});
return RestResponse.builder()

View File

@ -49,6 +49,11 @@ public class UserRestController {
log.info("userName: {}", userName);
return ResponseEntity.ok(userService.findByUniqUserName(userName));
}
@GetMapping("/api/admin/approval/search/name")
public ResponseEntity<?> findByUniqApprovalUser(@RequestParam String userName) {
log.info("userName: {}", userName);
return ResponseEntity.ok(userService.findByUniqApprovalUser(userName));
}
// 코드 그룹 수정 메서드
@PutMapping("/api/admin/user/{uniqId}")

View File

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itn.admin.itn.bizTrip.mapper.BizTripMapper">
<insert id="insertBizTrip" useGeneratedKeys="true" keyProperty="tripId">
INSERT INTO biz_trip (
trip_type_cd, location_cd, location_txt, purpose,
move_cd, trip_dt, start_time, end_time, status,
frst_register_id
) VALUES (
#{tripTypeCd}, #{locationCd}, #{locationTxt}, #{purpose},
#{moveCd}, #{tripDt}, #{startTime}, #{endTime}, #{status},
#{frstRegisterId}
)
</insert>
<insert id="insertTripMember">
INSERT INTO biz_trip_member (trip_id, uniq_id, role)
VALUES (#{tripId}, #{uniqId}, #{role})
</insert>
<insert id="insertApprovalLine">
INSERT INTO biz_trip_approval (trip_id, approver_id, order_no, approve_status)
VALUES (#{tripId}, #{approverId}, #{orderNo}, #{approveStatus})
</insert>
<select id="selectTripList" resultType="bizTripVO">
SELECT
bt.trip_id,
bt.trip_type_cd,
bt.location_cd,
bt.location_txt,
bt.purpose,
bt.move_cd,
bt.trip_dt,
bt.start_time,
bt.end_time,
bt.frst_register_id,
-- 상태 계산 로직
CASE
WHEN EXISTS (
SELECT 1
FROM biz_trip_approval a
WHERE a.trip_id = bt.trip_id
AND a.approve_status = '40'
) THEN '40' -- 반려
WHEN NOT EXISTS (
SELECT 1
FROM biz_trip_approval a
WHERE a.trip_id = bt.trip_id
AND a.approve_status <> '30'
) THEN '30' -- 전체 승인
WHEN EXISTS (
SELECT 1
FROM biz_trip_approval a
WHERE a.trip_id = bt.trip_id
AND a.approve_status <> '10'
) THEN '20' -- 일부 결재함 (진행 중)
ELSE '10' -- 전부 대기
END AS status
FROM biz_trip bt
ORDER BY bt.trip_dt DESC
</select>
<!--
&lt;!&ndash; 모든 코드 그룹을 조회하는 쿼리 &ndash;&gt;
<select id="findAll" resultType="codeVO">
SELECT * FROM common_code
</select>
&lt;!&ndash; 특정 코드 그룹을 ID로 조회하는 쿼리 &ndash;&gt;
<select id="findById" parameterType="String" resultType="codeVO">
SELECT * FROM common_code WHERE code_group_id = #{codeGroupId}
</select>
&lt;!&ndash; 코드 그룹을 추가하는 쿼리 &ndash;&gt;
<insert id="insert" parameterType="codeVO">
INSERT INTO common_code (code_group_id, code_group_name, description, frst_register_id, frst_regist_pnttm, last_updusr_id, last_updt_pnttm)
VALUES (#{codeGroupId}, #{codeGroupName}, #{description}, #{frstRegisterId}, #{frstRegistPnttm}, #{lastUpdusrId}, #{lastUpdtPnttm})
</insert>
&lt;!&ndash; 코드 그룹을 수정하는 쿼리 &ndash;&gt;
<update id="update" parameterType="codeVO">
UPDATE common_code
SET code_group_name = #{codeGroupName},
description = #{description},
last_updusr_id = #{lastUpdusrId},
last_updt_pnttm = #{lastUpdtPnttm}
WHERE code_group_id = #{codeGroupId}
</update>
&lt;!&ndash; 코드 그룹을 삭제하는 쿼리 &ndash;&gt;
<delete id="delete" parameterType="String">
DELETE FROM common_code WHERE code_group_id = #{codeGroupId}
</delete>-->
</mapper>

View File

@ -12,7 +12,7 @@
, user_id
, user_pw as password
, user_name
, user_rank
, rank_cd
, role
, gw_id
, biostar_id
@ -33,7 +33,7 @@
, user_id
, user_pw AS password
, user_name
, user_rank
, rank_cd
, mobile_phone
, dept_cd
, role
@ -57,8 +57,8 @@
<update id="updateUserInfo" parameterType="userVO">
UPDATE users SET
user_name = #{username}
, user_rank = #{userRank}
user_name = #{userName}
, rank_cd = #{rankCd}
, dept_cd = #{deptCd}
<if test="role != null and role != ''">
,role = #{role}
@ -66,6 +66,9 @@
<if test="gwId != null and gwId != ''">
,gw_id = #{gwId}
</if>
<if test="mobilePhone != null and mobilePhone != ''">
,mobile_phone = #{mobilePhone}
</if>
<if test="biostarId != null and biostarId != ''">
,biostar_id = #{biostarId}
</if>
@ -83,4 +86,20 @@
WHERE uniq_id = #{uniqId}
</update>
<select id="selectUsersByNameAndRank" resultType="userVO">
SELECT u.*
FROM users u
JOIN common_code_detail ccd
on u.rank_cd = ccd.code_id
WHERE ccd.code_group_id = 'RANK'
AND ccd.sort_order &lt;= (
SELECT sort_order
FROM common_code_detail
WHERE code_group_id = 'RANK'
AND code_name = '차장'
)
AND u.user_name LIKE CONCAT('%', #{userName}, '%')
AND u.active_yn = 'Y'
</select>
</mapper>

View File

@ -25,6 +25,10 @@
<typeAlias type="com.itn.admin.itn.commute.mapper.domain.ItnCommuteBackVO" alias="itnCommuteBackVO"/>
<typeAlias type="com.itn.admin.itn.bizTrip.mapper.domain.BizTripVO" alias="bizTripVO"/>
<typeAlias type="com.itn.admin.itn.bizTrip.mapper.domain.BizTripMemberVO" alias="bizTripMemberVO"/>
<typeAlias type="com.itn.admin.itn.bizTrip.mapper.domain.BizTripApprovalVO" alias="bizTripApprovalVO"/>
<typeAlias type="com.itn.admin.gw.holiday.mapper.domain.HolidayVO" alias="holidayVO"/>
</typeAliases>

View File

@ -1,177 +0,0 @@
$(function () {
$(".slider").each(function () {
var $slider = $(this); // 현재 슬라이더 요소
var $input = $slider.closest('.input-group').find('.sliderValue'); // 해당 슬라이더의 input 요소
// 슬라이더 초기화
$slider.slider({
range: "max",
min: 1,
max: 1000000,
value: 1,
slide: function (event, ui) {
$input.val(ui.value); // 슬라이더 이동 시 input 값 업데이트
}
});
// 슬라이더의 초기 값을 input에 설정
$input.val($slider.slider("value"));
// input 변경 시 슬라이더 값 업데이트 (실시간)
$input.on("input", function () {
var value = $(this).val();
// 숫자 범위 확인 후 슬라이더 값 업데이트
if ($.isNumeric(value) && value >= 1 && value <= 1000000) {
$slider.slider("value", value);
}
});
});
$("form").on("reset", function () {
setTimeout(function () {
$("#divTwoSms #sendCnt").val(1); // 건수 필드 값을 1로 설정
$("#divTwoSms #slider").slider("value", 1); // 슬라이더 값도 1로 설정
}, 0); // setTimeout 사용 이유: reset 이벤트 후에 값 설정
});
/*
* 예시 버튼
* */
$(".examBtn").on("click", function () {
var tagId = getParentsId($(this));
var $recvPhone = $(tagId + ' .recvPhone');
var $sendPhone = $(tagId + ' .sendPhone');
var $msgType = $(tagId + ' .msgType');
var $message = $(tagId + ' .message');
var $subject = $(tagId + ' .subject');
// 기본 전화번호 설정
$recvPhone.val('01083584250');
$sendPhone.val('01083584250');
// 메시지 타입에 따른 메시지 설정
var msgType = $msgType.val();
var msg = generateMessage(msgType);
// 내용
$message.val(msg);
updateByteCount($message);
if (msgType === 'L'
||msgType === 'M'
||msgType === 'A'
||msgType === 'F'
) {
$subject.val('ITN SUBJECT');
}
});
function generateMessage(msgType) {
var messages = {
'S': 'ITN SMS test ',
'L': 'ITN LMS test ',
'M': 'ITN MMS message test ',
'A': 'ITN ',
'F': 'ITN '
};
// 타입이 위 값들고 같이 않으면 null 반환
return (messages[msgType] || '') + getNowDate();
}
$('.msgType').on('change', function() {
var msgType = $(this).val();
var tagId = getParentsId($(this));
// 제목
$(tagId+' .subject').closest('.form-group').hide();
// 파일 그룹
$(tagId+' .fileUploadGroup').hide();
$(tagId+' .fileUploadGroup input[type="file"]').val("");
if(msgType === 'L'
||msgType === 'A'
||msgType === 'F'
) {
$(tagId+' .subject').closest('.form-group').show();
}else if(msgType === 'M'){
$(tagId+' .subject').closest('.form-group').show();
$(tagId+' .fileUploadGroup').show();
}
var $message = $(tagId + ' .message');
$message.val(generateMessage(msgType));
});
$('.toggle-info-btn').on('click', function() {
var $card = $(this).closest('.card'); // 클릭한 버튼의 가장 가까운 부모 .card 요소 찾기
var $hiddenInfo = $card.find('.hidden-info'); // 해당 카드 내에서 .hidden-info 요소 찾기
$hiddenInfo.slideToggle(); // 애니메이션 효과를 추가하여 더 부드럽게 보이도록 함
var icon = $(this).find('i');
if ($hiddenInfo.is(':visible')) {
icon.removeClass('fa-info-circle').addClass('fa-times-circle');
} else {
icon.removeClass('fa-times-circle').addClass('fa-info-circle');
}
});
$('textarea').on('input', function() {
updateByteCount(this);
});
});
// function updateByteCount(textarea) {
// console.log('textarea : ', textarea);
// var text = $(textarea).val();
// var byteLength = new TextEncoder().encode(text).length;
// $(textarea).closest('.form-group').find('.byte-count').text(byteLength + ' bytes');
// }
function updateByteCount(textarea) {
var text = $(textarea).val();
var byteLength = calculateByteLength(text);
$(textarea).closest('.form-group').find('.byte-count').text(byteLength + ' bytes');
}
function calculateByteLength(text) {
var byteLength = 0;
for (var i = 0; i < text.length; i++) {
var charCode = text.charCodeAt(i);
if (charCode <= 0x007F) {
byteLength += 1; // 1 byte for ASCII characters
} else if (charCode <= 0x07FF) {
byteLength += 2; // 2 bytes for characters from U+0080 to U+07FF
} else {
byteLength += 2; // 2 bytes for characters from U+0800 and above (including Hangul)
}
}
return byteLength;
}
function getParentsId($obj){
var $col = $obj.closest('.col-md-6'); // 클릭한 버튼의 가장 가까운 부모 .card 요소 찾기
// 해당 카드 내에서 .hidden-info 요소 찾기
return '#' + $col.attr('id');
}
function getNowDate(){
// 현재 날짜와 시간을 가져와서 포맷팅
var now = new Date();
var year = String(now.getFullYear()).substring(2); // 년도 마지막 두 자리
var month = ('0' + (now.getMonth() + 1)).slice(-2); // 월 (0부터 시작하므로 +1 필요)
var day = ('0' + now.getDate()).slice(-2); // 일
var hours = ('0' + now.getHours()).slice(-2); // 시
var minutes = ('0' + now.getMinutes()).slice(-2); // 분
return year + month + day + '|' + hours + ':' + minutes;
}

View File

@ -1,272 +0,0 @@
// 타이머 ID 저장을 위한 변수
let oneInsertCntIntervalId;
let oneTransferCntIntervalId;
let oneReporingCntIntervalId;
// insert 타이머
let oneIntervalId_insertSeconds;
// 이관 타이머
let oneIntervalId_transferSeconds;
// 이관 타이머
let oneIntervalId_reporingSeconds;
function fn_oneInsertScriptStart(){
// 건수를 현황확인으로 이동
$('#divOneSmsCard .sendCntTxt').text('('+$('#divOneSms .sliderValue').val()+'건)');
oneStartInsertTimer(); // insert 타임어택 시작
}
// LOG 테이블에
function fn_oneReportScriptStart(){
oneStartReportTimer(); // report 타임어택 시작
}
function oneStartInsertTimer() {
console.log(' :: startInsertTimer :: ');
let startTime = Date.now();
oneStartInsertCntTimer();
oneIntervalId_insertSeconds = setInterval(function() {
let currentTime = Date.now();
let elapsedTime = (currentTime - startTime) / 1000; // 밀리초를 초 단위로 변환
// 분과 초로 변환
let minutes = Math.floor(elapsedTime / 60); // 분 계산
let seconds = (elapsedTime % 60).toFixed(3); // 나머지 초 계산
document.querySelector('#divOneSmsCard .insertSeconds').innerText = minutes + ' 분 ' + seconds + ' 초';
}, 1);
}
function oneStartInsertCntTimer() {
console.log('oneStartInsertCntTimer ::');
// 1초마다 fn_insertCntAndTime 함수를 호출
oneInsertCntIntervalId = setInterval(fn_oneInsertCntAndTime, 1000);
}
function oneStopInsertTimer() {
clearInterval(oneIntervalId_insertSeconds);
clearInterval(oneInsertCntIntervalId);
console.log("insert 타이머가 멈췄습니다.");
}
function fn_oneInsertCntAndTime(){
console.log('fn_oneInsertCntAndTime ::');
// 폼 데이터를 수집
var formData = new FormData($("#divOneSms .sendForm")[0]);
var jsonObject = {};
formData.forEach((value, key) => {
if (!(value instanceof File)) {
jsonObject[key] = value;
}
});
console.log('url : /agent/one/findByInsertCnt');
$.ajax({
type: "POST",
url: "/agent/one/findByInsertCnt",
data: JSON.stringify(jsonObject), // JSON 문자열로 변환된 데이터를 전송
dataType: 'json',
contentType: 'application/json',
// async: true,
success: function (data) {
console.log(' one findByInsertCnt data : ', data);
if (data.status === 'OK') {
var cnt = data.data;
$('#divOneSmsCard .insertCnt').text(cnt);
let text = $('#divOneSmsCard .sendCntTxt').text();
let numberOnly = text.match(/\d+/)[0];
console.log(' one numberOnly :', numberOnly);
console.log(' one cnt >= numberOnly :', cnt >= numberOnly);
if(cnt >= numberOnly){
oneStopInsertTimer();
// oneStartTransferTimer($('#oneUserId').val()); // 이관 카운트
// fn_oneReportScriptStart();
}
}
else {
alert("오류 알림 : :: "+data.msg);
}
},
error: function (e) {
alert(" findByInsertCnt 조회에 실패하였습니다.");
oneStopInsertTimer();
console.log("ERROR : " + JSON.stringify(e));
},
beforeSend : function(xmlHttpRequest) {
},
complete : function(xhr, textStatus) {
//로딩창 hide
}
});
}
// 이관 타이머 start
function oneStartTransferTimer(userId) {
console.log(' :: one startTransferTimer :: ');
let startTime = Date.now();
oneStartTransferCntTimer(userId)
oneIntervalId_transferSeconds = setInterval(function() {
let currentTime = Date.now();
let elapsedTime = (currentTime - startTime) / 1000; // 밀리초를 초 단위로 변환
document.querySelector('#divOneSmsCard .transferSeconds').innerText = elapsedTime.toFixed(3) + ' 초';
}, 1);
}
function oneStartTransferCntTimer(userId) {
// 1초마다 fn_tranferCntAndTime 함수를 호출
oneTransferCntIntervalId = setInterval(function() {
fn_oneTranferCntAndTime(userId);
}, 1000);
}
function oneStopTransferTimer() {
clearInterval(oneIntervalId_transferSeconds);
clearInterval(oneTransferCntIntervalId);
console.log("이관 타이머가 멈췄습니다.");
}
function fn_oneTranferCntAndTime(userId){
// 폼 데이터를 수집
var formData = new FormData($("#divOneSms .sendForm")[0]);
var jsonObject = {};
formData.forEach((value, key) => {
jsonObject[key] = value;
});
jsonObject['userId'] = userId;
console.log('jsonObject : ', jsonObject);
$.ajax({
type: "POST",
url: "/agent/server/findByTransferCnt",
data: JSON.stringify(jsonObject), // JSON 문자열로 변환된 데이터를 전송
dataType: 'json',
contentType: 'application/json',
// async: true,
success: function (data) {
console.log('tranfer data : ', data);
if (data.status == 'OK') {
var cnt = data.data;
$('#divOneSmsCard .transferCnt').text(cnt);
let text = $('#divOneSmsCard .insertCnt').text();
let numberOnly = text.match(/\d+/)[0];
if(cnt >= Number(numberOnly)){
oneStopTransferTimer();
}
}
else {
alert("오류 알림 : :: "+data.msg);
}
},
error: function (e) {
alert("이관 조회에 실패하였습니다.");
console.log("ERROR : " + JSON.stringify(e));
},
beforeSend : function(xmlHttpRequest) {
},
complete : function(xhr, textStatus) {
//로딩창 hide
}
});
}
// 리포트 영역
// 리포트 영역
// 리포트 영역
function oneStartReportTimer(userId) {
console.log(' :: startReportTimer :: ');
let startTime = Date.now();
oneStartReporingCntTimer();
oneIntervalId_reporingSeconds = setInterval(function() {
let currentTime = Date.now();
let elapsedTime = (currentTime - startTime) / 1000; // 밀리초를 초 단위로 변환
document.querySelector('#divOneSmsCard .reportSeconds').innerText = elapsedTime.toFixed(3) + ' 초';
}, 1);
}
function oneStartReporingCntTimer() {
// 1초마다 fn_twoReportCntAndTime 함수를 호출
oneReporingCntIntervalId = setInterval(function() {
fn_oneReportCntAndTime();
}, 1000);
}
function oneStopReporingTimer() {
clearInterval(oneIntervalId_reporingSeconds);
clearInterval(oneReporingCntIntervalId);
console.log("report 타이머가 멈췄습니다.");
}
function fn_oneReportCntAndTime(userId){
// 폼 데이터를 수집
var formData = new FormData($("#divOneSms .sendForm")[0]);
var jsonObject = {};
formData.forEach((value, key) => {
if (!(value instanceof File)) {
jsonObject[key] = value;
}
});
jsonObject['userId'] = userId;
$.ajax({
type: "POST",
url: "/agent/one/findByLogMoveCntWhereMessage",
data: JSON.stringify(jsonObject), // JSON 문자열로 변환된 데이터를 전송
dataType: 'json',
contentType: 'application/json',
// async: true,
success: function (data) {
console.log('findByLogMoveCntWhereMessage data : ', data);
if (data.status == 'OK') {
var cnt = data.data;
$('#divOneSmsCard .reportStartCnt').text(cnt);
var transferCnt = $('#divOneSmsCard .insertCnt').text();
console.log('cnt : ', cnt);
console.log('reportStartCnt : ', transferCnt);
console.log('');
if(cnt >= Number(transferCnt)){
oneStopReporingTimer();
}
}
else {
alert("오류 알림 : :: "+data.msg);
}
},
error: function (e) {
alert("report 조회에 실패하였습니다.");
console.log("ERROR : " + JSON.stringify(e));
},
beforeSend : function(xmlHttpRequest) {
},
complete : function(xhr, textStatus) {
//로딩창 hide
}
});
}

View File

@ -1,273 +0,0 @@
// 타이머 ID 저장을 위한 변수
let twoInsertCntIntervalId;
let twoTransferCntIntervalId;
let twoReporingCntIntervalId;
// insert 타이머
let twoIntervalId_insertSeconds;
// 이관 타이머
let twoIntervalId_transferSeconds;
// 리포트 타이머
let twoIntervalId_reporingSeconds;
function fn_twoInsertScriptStart(){
// 건수를 현황확인으로 이동
$('#divTwoSmsCard .sendCntTxt').text('('+$('#divTwoSms .sliderValue').val()+'건)');
twoStartInsertTimer(); // insert 타임어택 시작
}
function fn_twoReportScriptStart(){
twoStartReportTimer(); // report 타임어택 시작
}
function twoStartInsertTimer() {
console.log(' :: startInsertTimer :: ');
let startTime = Date.now();
twoStartInsertCntTimer();
twoIntervalId_insertSeconds = setInterval(function() {
let currentTime = Date.now();
let elapsedTime = (currentTime - startTime) / 1000; // 밀리초를 초 단위로 변환
// 분과 초로 변환
let minutes = Math.floor(elapsedTime / 60); // 분 계산
let seconds = (elapsedTime % 60).toFixed(3); // 나머지 초 계산
document.querySelector('#divTwoSmsCard .insertSeconds').innerText = minutes + ' 분 ' + seconds + ' 초';
}, 1);
}
function twoStartInsertCntTimer() {
// 1초마다 fn_insertCntAndTime 함수를 호출
twoInsertCntIntervalId = setInterval(fn_twoInsertCntAndTime, 1000);
}
function twoStopInsertTimer() {
clearInterval(twoIntervalId_insertSeconds);
clearInterval(twoInsertCntIntervalId);
console.log("insert 타이머가 멈췄습니다.");
}
function fn_twoInsertCntAndTime(){
// 폼 데이터를 수집
var formData = new FormData($("#divTwoSms .sendForm")[0]);
var jsonObject = {};
formData.forEach((value, key) => {
if (!(value instanceof File)) {
jsonObject[key] = value;
}
});
console.log('fn_twoInsertCntAndTime : [{}]',jsonObject);
$.ajax({
type: "POST",
url: "/agent/two/findByInsertCnt",
data: JSON.stringify(jsonObject), // JSON 문자열로 변환된 데이터를 전송
dataType: 'json',
contentType: 'application/json',
// async: true,
success: function (data) {
// console.log('insert data : ', data);
if (data.status == 'OK') {
var cnt = data.data;
$('#divTwoSmsCard .insertCnt').text(cnt);
let text = $('#divTwoSmsCard .sendCntTxt').text();
let numberOnly = text.match(/\d+/)[0];
// console.log('numberOnly :', numberOnly);
// console.log('cnt >= numberOnly :', cnt >= numberOnly);
if(cnt >= numberOnly){
twoStopInsertTimer();
// twoStartTransferTimer($('#twoUserId').val()); // 이관 카운트
// fn_twoReportScriptStart();
}
}
else {
alert("오류 알림 : :: "+data.msg);
}
},
error: function (e) {
alert("조회에 실패하였습니다.");
console.log("ERROR : " + JSON.stringify(e));
},
beforeSend : function(xmlHttpRequest) {
},
complete : function(xhr, textStatus) {
//로딩창 hide
}
});
}
// 이관 타이머 start
function twoStartTransferTimer(userId) {
console.log(' :: two startTransferTimer :: ');
let startTime = Date.now();
twoStartTransferCntTimer(userId)
twoIntervalId_transferSeconds = setInterval(function() {
let currentTime = Date.now();
let elapsedTime = (currentTime - startTime) / 1000; // 밀리초를 초 단위로 변환
// console.log('elapsedTime : ', elapsedTime);
document.querySelector('#divTwoSmsCard .transferSeconds').innerText = elapsedTime.toFixed(3) + ' 초';
}, 1);
}
function twoStartTransferCntTimer(userId) {
// 1초마다 fn_tranferCntAndTime 함수를 호출
twoTransferCntIntervalId = setInterval(function() {
fn_twoTranferCntAndTime(userId);
}, 1000);
}
function twoStopTransferTimer() {
clearInterval(twoIntervalId_transferSeconds);
clearInterval(twoTransferCntIntervalId);
console.log("이관 타이머가 멈췄습니다.");
}
function fn_twoTranferCntAndTime(userId){
// 폼 데이터를 수집
var formData = new FormData($("#divTwoSms .sendForm")[0]);
// console.log('? :: ', formData);
var jsonObject = {};
formData.forEach((value, key) => {
jsonObject[key] = value;
});
jsonObject['userId'] = userId;
// console.log('jsonObject : ', jsonObject);
$.ajax({
type: "POST",
url: "/agent/server/findByTransferCnt",
data: JSON.stringify(jsonObject), // JSON 문자열로 변환된 데이터를 전송
dataType: 'json',
contentType: 'application/json',
// async: true,
success: function (data) {
console.log('tranfer data : ', data);
if (data.status == 'OK') {
var cnt = data.data;
$('#divTwoSmsCard .transferCnt').text(cnt);
let text = $('#divTwoSmsCard .insertCnt').text();
let numberOnly = text.match(/\d+/)[0];
if(cnt >= Number(numberOnly)){
twoStopTransferTimer();
}
}
else {
alert("오류 알림 : :: "+data.msg);
}
},
error: function (e) {
alert("이관 조회에 실패하였습니다.");
console.log("ERROR : " + JSON.stringify(e));
},
beforeSend : function(xmlHttpRequest) {
},
complete : function(xhr, textStatus) {
//로딩창 hide
}
});
}
// 리포트 영역
// 리포트 영역
// 리포트 영역
function twoStartReportTimer() {
// console.log(' :: startReportTimer :: ');
let startTime = Date.now();
twoStartReporingCntTimer();
twoIntervalId_reporingSeconds = setInterval(function() {
let currentTime = Date.now();
let elapsedTime = (currentTime - startTime) / 1000; // 밀리초를 초 단위로 변환
document.querySelector('#divTwoSmsCard .reportSeconds').innerText = elapsedTime.toFixed(3) + ' 초';
}, 1);
}
function twoStartReporingCntTimer() {
// 1초마다 fn_twoReportCntAndTime 함수를 호출
twoReporingCntIntervalId = setInterval(function() {
fn_twoReportCntAndTime();
}, 1000);
}
function twoStopReporingTimer() {
clearInterval(twoIntervalId_reporingSeconds);
clearInterval(twoReporingCntIntervalId);
console.log("report 타이머가 멈췄습니다.");
}
function fn_twoReportCntAndTime(userId){
// 폼 데이터를 수집
var formData = new FormData($("#divTwoSms .sendForm")[0]);
var jsonObject = {};
formData.forEach((value, key) => {
if (!(value instanceof File)) {
jsonObject[key] = value;
}
});
jsonObject['userId'] = userId;
$.ajax({
type: "POST",
url: "/agent/two/findByLogMoveCntWhereMessage",
data: JSON.stringify(jsonObject), // JSON 문자열로 변환된 데이터를 전송
dataType: 'json',
contentType: 'application/json',
// async: true,
success: function (data) {
console.log('findByLogMoveCntWhereMessage data : ', data);
if (data.status == 'OK') {
var cnt = data.data;
console.log('cnt : ', cnt);
// 리포트 영역에 cnt 추가
$('#divTwoSmsCard .reportStartCnt').text(cnt);
// server DB에 update한 건수와 cnt비교
var transferCnt = $('#divTwoSmsCard .insertCnt').text();
console.log('cnt : ', cnt);
console.log('reportStartCnt : ', transferCnt);
console.log('');
if(cnt >= Number(transferCnt)){
twoStopReporingTimer();
}
}
else {
alert("오류 알림 : :: "+data.msg);
}
},
error: function (e) {
alert("report 조회에 실패하였습니다.");
console.log("ERROR : " + JSON.stringify(e));
},
beforeSend : function(xmlHttpRequest) {
},
complete : function(xhr, textStatus) {
//로딩창 hide
}
});
}

View File

@ -0,0 +1,247 @@
/**
* 페이지 로드 완료 실행되는 초기화 함수 (비워둠)
* @function
*/
$(function () {
// 초기화 코드 필요 시 여기에 작성
});
/**
* Enter 입력 사용자 검색 수행
* @event keydown
* @param {KeyboardEvent} e
*/
document.getElementById("userSearchKeyword").addEventListener("keydown", function (e) {
if (e.key === "Enter") {
e.preventDefault();
searchUser();
}
});
document.getElementById("approvalSearchKeyword").addEventListener("keydown", function (e) {
if (e.key === "Enter") {
e.preventDefault();
searchApproval();
}
});
/**
* 모달이 열릴 사용자 목록을 불러오고 포커스를 맞춤
* @event shown.bs.modal
*/
$('#userSearchModal').on('shown.bs.modal', function () {
loadUserList();
$('#userSearchKeyword').trigger('focus');
});
$('#approvalSearchModal').on('shown.bs.modal', function () {
loadApprovalList();
$('#approvalSearchKeyword').trigger('focus');
});
/**
* 날짜 입력 필드 클릭 브라우저 기본 날짜 선택기 표시
* @event click
*/
document.querySelector('input[id="tripDate"]').addEventListener('click', function () {
this.showPicker && this.showPicker();
});
/**
* 사용자 목록을 불러오는 Ajax 요청 함수
* @function
* @param {string} [keyword=""] - 검색 키워드
* @returns {void}
*/
function loadUserList(keyword = "") {
$.ajax({
url: '/api/admin/user/search/name',
type: 'GET',
data: { userName: keyword },
success: function (result) {
const tbody = document.getElementById("userSearchResult");
tbody.innerHTML = "";
const data = result.data;
if (data && data.length > 0) {
data.forEach(user => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${user.userName}</td>
<td>${user.deptNm || "-"}</td>
<td>${user.rankCd || "-"}</td>
<td>
<button class="btn btn-sm btn-success"
onclick="selectUser('${user.userName}', '${user.deptNm}', '${user.mobilePhone}', '${user.uniqId}')">
<i class="fas fa-user-plus"></i>
</button>
</td>
`;
tbody.appendChild(row);
});
} else {
tbody.innerHTML = `<tr><td colspan="5" class="text-center text-muted">검색 결과가 없습니다.</td></tr>`;
}
},
error: function () {
alert("사용자 목록을 불러오는 데 실패했습니다.");
}
});
}
function loadApprovalList(keyword = "") {
$.ajax({
url: '/api/admin/approval/search/name',
type: 'GET',
data: { userName: keyword },
success: function (result) {
const tbody = document.getElementById("approvalSearchResult");
const data = result.data;
tbody.innerHTML = "";
if (data && data.length > 0) {
data.forEach(user => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${user.userName}</td>
<td>${user.deptNm || "-"}</td>
<td>${user.rankCd || "-"}</td>
<td>
<button class="btn btn-sm btn-success"
onclick="selectApproval('${user.userName}', '${user.deptNm}', '${user.mobilePhone}', '${user.uniqId}')">
<i class="fas fa-user-plus"></i>
</button>
</td>
`;
tbody.appendChild(row);
});
} else {
tbody.innerHTML = `<tr><td colspan="5" class="text-center text-muted">검색 결과가 없습니다.</td></tr>`;
}
},
error: function () {
alert("사용자 목록을 불러오는 데 실패했습니다.");
}
});
}
/**
* 사용자를 선택하여 출장 인원 또는 결재라인에 추가
* @function
* @param {string} name - 사용자 이름
* @param {string} dept - 부서명
* @param {string} phone - 전화번호
* @param {string} uniqId - 사용자 고유 ID
* @returns {void}
*/
function selectUser(name, dept, phone, uniqId) {
const tbody = document.getElementById("tripMemberTbody");
const newRow = document.createElement("tr");
newRow.setAttribute("data-uniqid", uniqId);
newRow.innerHTML = `
<td>${name}</td>
<td>${dept}</td>
<td>${phone}</td>
<td class="text-center">
<a href="#" class="btn btn-danger btn-sm" onclick="removeMemberRow(this)">
<i class="fas fa-user-minus"></i>
</a>
</td>
`;
tbody.appendChild(newRow);
$('#userSearchModal').modal('hide');
}
function selectApproval(name, dept, phone, uniqId) {
const stageId = $('#approvalSearchModal').data('stageId');
const rowMap = {
approval1: { index: 0, inputId: "approver1" },
approval2: { index: 1, inputId: "approver2" },
approval3: { index: 2, inputId: "approver3" }
};
const { index, inputId } = rowMap[stageId];
const tbody = document.getElementById("approvalLineTbody");
const targetRow = tbody.rows[index];
if (!targetRow) return;
targetRow.innerHTML = `
<td class="text-center align-middle">${targetRow.cells[0].textContent}</td>
<td>${name}</td>
<td>${dept}</td>
<td>${phone}</td>
<td class="text-center">
<a href="#" class="btn btn-danger btn-sm" onclick="resetApproval('${stageId}')">
<i class="fas fa-user-minus"></i>
</a>
</td>
`;
// hidden input에 uniqId 저장
document.getElementById(inputId).value = uniqId;
$('#approvalSearchModal').modal('hide');
$('#approvalSearchModal').data('stageId', null);
}
/**
* 검색창 버튼 클릭 사용자 검색 실행
* @function
*/
function searchUser() {
const keyword = document.getElementById("userSearchKeyword").value.trim();
loadUserList(keyword);
}
function searchApproval() {
const keyword = document.getElementById("userSearchKeyword").value.trim();
loadApprovalList(keyword);
}
/**
* 출장 인원 삭제
* @function
* @param {HTMLElement} el - 삭제 버튼 요소
*/
function removeMemberRow(el) {
if (confirm("정말 삭제하시겠습니까?")) {
const row = el.closest("tr");
if (row) row.remove();
}
}
/**
* 결재자 지정 모달 열기
* @function
* @param {string} stageId - 결재 단계 ID (approval1, approval2, approval3)
*/
function openApprovalModal(stageId) {
$('#approvalSearchModal').data('stageId', stageId).modal('show');
}
/**
* 결재자 삭제 단계 초기화
* @function
* @param {string} stageId - 결재 단계 ID
*/
function resetApproval(stageId) {
const rowIndex = {
approval1: 0,
approval2: 1,
approval3: 2
}[stageId];
const tbody = document.getElementById("approvalLineTbody");
const label = ['검토 1', '검토 2', '결제'][rowIndex];
tbody.rows[rowIndex].innerHTML = `
<td class="text-center align-middle">${label}</td>
<td colspan="4">
<button type="button" class="btn btn-outline-info btn-sm" onclick="openApprovalModal('${stageId}')">
<i class="fas fa-user-plus"></i>
</button>
</td>
`;
}

View File

@ -0,0 +1,26 @@
$(function () {
/**
* 시작 시간 선택기 초기화
* @function
*/
$('#startTimePicker').datetimepicker({
format: 'HH:mm',
stepping: 10
});
/**
* 종료 시간 선택기 초기화
* @function
*/
$('#endTimePicker').datetimepicker({
format: 'HH:mm',
stepping: 10,
icons: {
time: 'far fa-clock',
up: 'fas fa-chevron-up',
down: 'fas fa-chevron-down'
}
});
});

View File

@ -0,0 +1,52 @@
/**
* 출장 등록 전송 함수 ( 생성 + 전송 담당)
*/
function collectAndSubmitTripData() {
if (!validateTripForm()) return;
// 값 생성
const tripInfo = {
tripTypeCd: $('#tripType').val(),
locationCd: $('#tripLocation').val(),
locationTxt: $('#locationTxt').val(),
purpose: $('#purpose').val(),
moveCd: $('#tripMove').val(),
tripDt: $('#tripDate').val(),
startTime: $('#startTimePicker input').val(),
endTime: $('#endTimePicker input').val(),
status: '10'
};
const tripMembers = [];
$('#tripMemberTbody tr').each(function () {
const uniqId = $(this).data('uniqid');
if (!uniqId) return;
const role = $(this).find('td').eq(3).text().trim() === '기안자' ? '0' : '1';
tripMembers.push({ uniqId, role });
});
const approvalLines = [];
['approver1', 'approver2', 'approver3'].forEach((id, idx) => {
const uniqId = $(`#${id}`).val();
if (uniqId) {
approvalLines.push({
approverId: uniqId,
orderNo: idx + 1,
approveStatus: '10'
});
}
});
const payload = { tripInfo, tripMembers, approvalLines };
console.log("payload:", payload);
// Ajax 전송
$.ajax({
url: '/api/bizTrip/register',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(payload),
success: () => fn_successAlert("등록 성공", "출장 정보가 저장되었습니다."),
error: () => fn_failedAlert("등록 실패", "오류가 발생했습니다.")
});
}

View File

@ -0,0 +1,65 @@
/**
* 출장 등록 입력값 유효성 검사 함수
* - 필수 항목 누락 여부 확인
* - 출장일자가 오늘 이전인지 확인
* - 시작 시간이 종료 시간보다 늦은지 확인
*
* @returns {boolean} 유효하면 true, 유효하지 않으면 false
*/
function validateTripForm() {
// ===== [1] 입력값 가져오기 =====
const tripType = $('#tripType');
const location = $('#tripLocation');
const locationTxt = $('#locationTxt');
const purpose = $('#purpose');
const move = $('#tripMove');
const date = $('#tripDate');
const start = $('#startTimePicker input');
const end = $('#endTimePicker input');
const approver = $('#approver3');
const tripTypeCd = tripType.val();
const locationCd = location.val();
const locationTxtVal = locationTxt.val();
const purposeVal = purpose.val();
const moveCd = move.val();
const tripDt = date.val();
const startTime = start.val();
const endTime = end.val();
const approver3 = approver.val();
// ===== [2] 필수값 체크 + focus =====
if (!tripTypeCd) return fn_failedAlert("입력 오류", "출장 구분을 선택해주세요.", 1000), tripType.focus(), false;
if (!locationCd) return fn_failedAlert("입력 오류", "출장지를 선택해주세요.", 1000), location.focus(), false;
if (!locationTxtVal || locationTxtVal.trim() === "") return fn_failedAlert("입력 오류", "목적지를 입력해주세요.", 1000), locationTxt.focus(), false;
if (!purposeVal || purposeVal.trim() === "") return fn_failedAlert("입력 오류", "출장 목적을 입력해주세요.", 1000), purpose.focus(), false;
if (!moveCd) return fn_failedAlert("입력 오류", "이동 수단을 선택해주세요.", 1000), move.focus(), false;
if (!tripDt) return fn_failedAlert("입력 오류", "출장일자를 선택해주세요.", 1000), date.focus(), false;
if (!startTime || !endTime) return fn_failedAlert("입력 오류", "시작/종료 시간을 입력해주세요.", 1000), start.focus(), false;
if (!approver3) return fn_failedAlert("입력 오류", "최종 결재자를 지정해주세요.", 1000), approver.focus(), false;
// ===== [3] 출장일자가 오늘보다 과거일 경우 =====
const today = new Date();
const inputDate = new Date(tripDt);
today.setHours(0, 0, 0, 0);
inputDate.setHours(0, 0, 0, 0);
if (inputDate < today) {
fn_failedAlert("입력 오류", "출장일자는 오늘보다 빠를 수 없습니다.");
date.focus();
return false;
}
// ===== [4] 시간 순서 확인 (종료 > 시작) =====
const [sH, sM] = startTime.split(':').map(Number);
const [eH, eM] = endTime.split(':').map(Number);
const startDate = new Date(); startDate.setHours(sH, sM, 0, 0);
const endDate = new Date(); endDate.setHours(eH, eM, 0, 0);
if (endDate <= startDate) {
fn_failedAlert("입력 오류", "종료 시간은 시작 시간보다 이후여야 합니다.");
end.focus();
return false;
}
// ===== [5] 유효성 통과 =====
return true;
}

View File

@ -37,7 +37,7 @@
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" th:href="@{/plugins/toastr/toastr.min.css}">
<!-- CSS -->
<!-- jQuery -->
@ -121,6 +121,16 @@
body: msg
})
}
function fn_failedAlert(title, msg, delay){
$(document).Toasts('create', {
class: 'bg-danger',
title: title,
subtitle: '',
autohide : true,
delay: delay,
body: msg
})
}
</script>
</head>

View File

@ -92,7 +92,7 @@
</a>
<ul class="nav nav-treeview">
<li class="nav-item">
<a th:href="@{/itn/trip/reg}" class="nav-link">
<a th:href="@{/itn/bizTrip/reg}" class="nav-link">
<!-- <i class="far fa-circle nav-icon"></i>-->
<!-- <i class="far fa-clock nav-icon"></i>-->
<i class="nav-icon fas fa-calendar-check"></i>

View File

@ -0,0 +1,220 @@
<!DOCTYPE html>
<!-- 관련 Namespace 선언 및 layout:decorate 추가 -->
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="layout">
<head>
<!-- layout.html 에 들어간 head 부분을 제외하고 개별 파일에만 적용되는 head 부분 추가 -->
<title>사용자 관리</title>
<!-- 필요하다면 개별 파일에 사용될 css/js 선언 -->
<link rel="stylesheet" th:href="@{/plugins/datatables-bs4/css/dataTables.bootstrap4.min.css}">
<link rel="stylesheet" th:href="@{/plugins/datatables-responsive/css/responsive.bootstrap4.min.css}">
<link rel="stylesheet" th:href="@{/plugins/datatables-buttons/css/buttons.bootstrap4.min.css}">
<style>
.cursor-pointer {
cursor: pointer;
}
.cursor-pointer:hover {
text-decoration: underline;
color: #007bff;
}
</style>
</head>
<body layout:fragment="body">
<div class="wrapper">
<div th:replace="~{fragments/top_nav :: topFragment}"/>
<!-- Main Sidebar Container -->
<aside class="main-sidebar sidebar-dark-primary elevation-4"
th:insert="~{fragments/mainsidebar :: sidebarFragment}">
</aside>
<!-- Content Wrapper. Contains page content -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0">사용자 관리</h1>
</div><!-- /.col -->
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="#">Home</a></li>
<li class="breadcrumb-item active">출퇴근 관리</li>
</ol>
</div><!-- /.col -->
</div><!-- /.row -->
</div><!-- /.container-fluid -->
</div>
<!-- /.content-header -->
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<div class="row">
<div class="col-12">
<!-- /.card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">목록</h3>
</div>
<!-- /.card-header -->
<div class="card-body">
<table id="tripTb" class="table table-bordered table-striped" style="width: 100%;">
<thead>
<tr>
<th>#</th>
<th>출장ID</th>
<th>출장일</th>
<th>시간</th>
<th>출장구분</th>
<th>지역</th>
<th>목적지</th>
<th>목적</th>
<th>이동수단</th>
<th>상태</th>
<th>결재상태</th>
<th>작성자</th>
</tr>
</thead>
<tbody>
<tr th:each="row, stat : ${list}">
<td th:text="${stat.count}"/>
<td th:text="${row.tripId}"/>
<td th:text="${#temporals.format(row.tripDt, 'yyyy-MM-dd')}"/>
<td th:text="${row.startTime + ' ~ ' + row.endTime}"/>
<td th:text="${@TCodeUtils.getCodeName('TRIP_TYPE', row.tripTypeCd)}"/>
<td th:text="${@TCodeUtils.getCodeName('TRIP_LOCATION', row.locationCd)}"/>
<td th:text="${row.locationTxt}"/>
<td th:text="${row.purpose}"/>
<td th:text="${@TCodeUtils.getCodeName('TRIP_MOVE', row.moveCd)}"/>
<td>
<span th:switch="${row.status}">
<span th:case="'10'" class="badge badge-warning">대기</span>
<span th:case="'20'" class="badge badge-primary">진행</span>
<span th:case="'30'" class="badge badge-success">승인</span>
<span th:case="'40'" class="badge badge-danger">반려</span>
<span th:case="*">-</span>
</span>
</td>
<td>
<span th:switch="${row.latestApproveStatus}">
<span th:case="'10'" class="badge badge-warning">대기</span>
<span th:case="'20'" class="badge badge-primary">진행</span>
<span th:case="'30'" class="badge badge-success">승인</span>
<span th:case="'40'" class="badge badge-danger">반려</span>
<span th:case="*">-</span>
</span>
</td>
<td th:text="${row.frstRegisterId}"/>
</tr>
</tbody>
</table>
</div>
<!-- /.card-body -->
</div>
<!-- /.card -->
</div>
<!-- /.col -->
</div>
<!-- /.row -->
</div>
<!-- /.container-fluid -->
</section>
<!-- /Main content -->
</div>
<!-- /.content-wrapper -->
<footer class="main-footer"
th:insert="~{fragments/footer :: footerFragment}">
</footer>
<!-- Control Sidebar -->
<aside class="control-sidebar control-sidebar-dark">
<!-- Control sidebar content goes here -->
</aside>
<!-- /.control-sidebar -->
</div>
<!-- ./wrapper -->
<!-- DataTables & Plugins -->
<script th:src="@{/plugins/datatables/jquery.dataTables.min.js}"></script>
<script th:src="@{/plugins/datatables-bs4/js/dataTables.bootstrap4.min.js}"></script>
<script th:src="@{/plugins/datatables-responsive/js/dataTables.responsive.min.js}"></script>
<script th:src="@{/plugins/datatables-responsive/js/responsive.bootstrap4.min.js}"></script>
<script th:src="@{/plugins/datatables-buttons/js/dataTables.buttons.min.js}"></script>
<script th:src="@{/plugins/datatables-buttons/js/buttons.bootstrap4.min.js}"></script>
<script th:src="@{/plugins/jszip/jszip.min.js}"></script>
<script th:src="@{/plugins/pdfmake/pdfmake.min.js}"></script>
<script th:src="@{/plugins/pdfmake/vfs_fonts.js}"></script>
<script th:src="@{/plugins/datatables-buttons/js/buttons.html5.min.js}"></script>
<script th:src="@{/plugins/datatables-buttons/js/buttons.print.min.js}"></script>
<script th:src="@{/plugins/datatables-buttons/js/buttons.colVis.min.js}"></script>
<script>
const commonExportOptions = {
columns: ':visible',
format: {
body: function (data, row, column, node) {
if ($(node).find('select').length) {
return $(node).find('select option:selected').text();
}
// 태그 제거: 보이는 텍스트만 반환
return $(node).text();
}
}
};
$("#tripTb").DataTable({
"responsive": true
, "lengthChange": false
, "autoWidth": false
, "pageLength": 20
, "buttons": [
{
extend: 'copy',
charset: 'UTF-8',
exportOptions: commonExportOptions
},
{
extend: 'csv',
charset: 'UTF-8',
bom: true,
exportOptions: commonExportOptions
},
{
extend: 'excel',
charset: 'UTF-8',
exportOptions: commonExportOptions
},
{
extend: 'pdf',
charset: 'UTF-8',
exportOptions: commonExportOptions
},
{
extend: 'print',
charset: 'UTF-8',
exportOptions: commonExportOptions
},
"colvis"]
}).buttons().container().appendTo('#commuteTb_wrapper .col-md-6:eq(0)');
</script>
</body>
</html>

View File

@ -6,7 +6,7 @@
layout:decorate="layout">
<head>
<!-- layout.html 에 들어간 head 부분을 제외하고 개별 파일에만 적용되는 head 부분 추가 -->
<title>사용자 관리</title>
<title>출장 등록</title>
<!-- 필요하다면 개별 파일에 사용될 css/js 선언 -->
@ -33,14 +33,6 @@
border-top: 2px solid #009fe3;
}
.btn-custom {
border: 1px solid #dcdcdc;
background-color: #ffffff;
color: #009fe3;
font-weight: bold;
padding: 6px 18px;
border-radius: 6px;
}
</style>
</head>
@ -81,7 +73,7 @@
<!-- 출장신청 영역 -->
<div class="card mb-4">
<div class="card-header">
출장신청
출장정보
</div>
<div class="card-body">
<table class="table table-bordered table-form" style="table-layout: fixed;">
@ -100,19 +92,19 @@
<th>출장지</th>
<td>
<select class="form-control d-inline" style="width: 20%;" id="tripLocation" name="tripLocation">
<option value="" th:selected>-- 선택 --</option>
<option value="" th:selected>-- 지역 --</option>
<option th:each="code : ${@TCodeUtils.getCodeList('TRIP_LOCATION')}"
th:value="${code.codeId}"
th:text="${code.codeName}">
</option>
</select>
<input type="text" class="form-control d-inline" style="width: 70%;">
<input type="text" id="locationTxt" class="form-control d-inline" style="width: 70%;" placeholder="목적지">
</td>
</tr>
<tr>
<th>출장목적</th>
<td><input type="text" class="form-control"></td>
<td><input type="text" id="purpose" class="form-control"></td>
<th>이동사항</th>
<td>
<select class="form-control" id="tripMove" name="tripMove">
@ -131,12 +123,30 @@
</td>
<th>시간</th>
<td>
<div class="d-flex gap-2">
<input type="time" id="startTime" class="form-control" style="margin-right: 4px;">
<span class="align-self-center">~</span>
<input type="time" id="endTime" class="form-control" style="margin-left: 4px;">
<div class="d-flex">
<!-- 시작 시간 -->
<div class="input-group date" id="startTimePicker" data-target-input="nearest" style="margin-right: 5px;">
<input type="text"
class="form-control datetimepicker-input"
data-target="#startTimePicker"
data-toggle="datetimepicker" />
<div class="input-group-append" data-target="#startTimePicker" data-toggle="datetimepicker">
<div class="input-group-text"><i class="far fa-clock"></i></div>
</div>
</div>
<span class="align-self-center mx-2">~</span>
<!-- 종료 시간 -->
<div class="input-group date" id="endTimePicker" data-target-input="nearest">
<input type="text"
class="form-control datetimepicker-input"
data-target="#endTimePicker"
data-toggle="datetimepicker" />
<div class="input-group-append" data-target="#endTimePicker" data-toggle="datetimepicker">
<div class="input-group-text"><i class="far fa-clock"></i></div>
</div>
</div>
</div>
</td>
</td>
</tr>
</tbody>
</table>
@ -163,9 +173,8 @@
</tr>
</thead>
<tbody id="tripMemberTbody">
<tr>
<tr th:data-uniqid="${loginUser.getUniqId()}">
<td th:text="${loginUser.getUserName()}"/>
<td th:text="${@TCodeUtils.getCodeName('DEPT', loginUser.getDeptCd())}"/>
<td th:text="${loginUser.getMobilePhone()}"/>
<td class="text-center">기안자</td>
@ -175,9 +184,57 @@
</div>
</div>
<!-- 결제 라인 영역 -->
<div class="card mt-4">
<div class="card-header">
결제라인
</div>
<div class="card-body p-0">
<table class="table table-bordered table-form mb-0">
<thead>
<tr class="text-center bg-light">
<th style="width: 20%;">결재 단계</th>
<th style="width: 20%;">이름</th>
<th style="width: 25%;">부서명</th>
<th style="width: 25%;">연락처</th>
<th style="width: 10%;" class="text-center">관리</th>
</tr>
</thead>
<tbody id="approvalLineTbody">
<tr>
<td class="text-center align-middle">검토 1</td>
<td colspan="4">
<button type="button" class="btn btn-outline-info btn-sm" onclick="openApprovalModal('approval1')">
<i class="fas fa-user-plus"></i> 사용자 지정
</button>
</td>
</tr>
<tr>
<td class="text-center align-middle">검토 2</td>
<td colspan="4">
<button type="button" class="btn btn-outline-info btn-sm" onclick="openApprovalModal('approval2')">
<i class="fas fa-user-plus"></i> 사용자 지정
</button>
</td>
</tr>
<tr>
<td class="text-center align-middle">결제</td>
<td colspan="4">
<button type="button" class="btn btn-outline-info btn-sm" onclick="openApprovalModal('approval3')">
<i class="fas fa-user-plus"></i> 사용자 지정
</button>
</td>
</tr>
</tbody>
</table>
</div>
<input type="hidden" id="approver1" name="approver1">
<input type="hidden" id="approver2" name="approver2">
<input type="hidden" id="approver3" name="approver3">
</div>
<div class="text-right mt-2">
<!-- 등록 버튼 -->
<button type="submit" class="btn btn-primary btn-sm">
<button type="submit" class="btn btn-primary btn-sm" onclick="collectAndSubmitTripData()">
<i class="fas fa-save"></i> 등록
</button>
</div>
@ -191,6 +248,52 @@
</div>
<!-- 사용자 검색 모달 -->
<div class="modal fade" id="approvalSearchModal" tabindex="-1" role="dialog" aria-labelledby="userSearchModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header bg-info">
<h5 class="modal-title text-white" id="approvalSearchModalLabel">사용자 검색</h5>
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<!-- 검색 영역 -->
<div class="form-inline mb-3">
<label class="mr-2">이름 검색</label>
<input type="text" class="form-control mr-2" auto id="approvalSearchKeyword" placeholder="검색어 입력" autocomplete="off">
<button type="button" class="btn btn-info btn-sm" onclick="searchUser()">검색</button>
</div>
<!-- 결과 테이블 -->
<div class="table-responsive" style="max-height: 500px; overflow-y: auto;">
<table class="table table-hover table-bordered table-sm text-center">
<thead class="thead-light">
<tr>
<th>이름</th>
<th>부서</th>
<th>직급</th>
<th>추가</th>
</tr>
</thead>
<tbody id="approvalSearchResult">
<!-- 검색 결과 동적 렌더링 -->
<!-- 예시 -->
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">닫기</button>
</div>
</div>
</div>
</div>
<!-- 사용자 검색 모달 -->
<div class="modal fade" id="userSearchModal" tabindex="-1" role="dialog" aria-labelledby="userSearchModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl" role="document">
@ -206,7 +309,7 @@
<!-- 검색 영역 -->
<div class="form-inline mb-3">
<label class="mr-2">이름 검색</label>
<input type="text" class="form-control mr-2" id="userSearchKeyword" placeholder="검색어 입력">
<input type="text" class="form-control mr-2" auto id="userSearchKeyword" placeholder="검색어 입력" autocomplete="off">
<button type="button" class="btn btn-info btn-sm" onclick="searchUser()">검색</button>
</div>
@ -249,127 +352,10 @@
</div>
<!-- ./wrapper -->
<!-- DataTables & Plugins -->
<script th:src="@{/plugins/datatables/jquery.dataTables.min.js}"></script>
<script th:src="@{/plugins/datatables-bs4/js/dataTables.bootstrap4.min.js}"></script>
<script th:src="@{/plugins/datatables-responsive/js/dataTables.responsive.min.js}"></script>
<script th:src="@{/plugins/datatables-responsive/js/responsive.bootstrap4.min.js}"></script>
<script th:src="@{/plugins/datatables-buttons/js/dataTables.buttons.min.js}"></script>
<script th:src="@{/plugins/datatables-buttons/js/buttons.bootstrap4.min.js}"></script>
<script th:src="@{/plugins/jszip/jszip.min.js}"></script>
<script th:src="@{/plugins/pdfmake/pdfmake.min.js}"></script>
<script th:src="@{/plugins/pdfmake/vfs_fonts.js}"></script>
<script th:src="@{/plugins/datatables-buttons/js/buttons.html5.min.js}"></script>
<script th:src="@{/plugins/datatables-buttons/js/buttons.print.min.js}"></script>
<script th:src="@{/plugins/datatables-buttons/js/buttons.colVis.min.js}"></script>
<script>
document.querySelector('input[id="startTime"]').addEventListener('click', function() {
this.showPicker && this.showPicker(); // 일부 브라우저에서 시간 선택기 강제 실행
});
document.querySelector('input[id="endTime"]').addEventListener('click', function() {
this.showPicker && this.showPicker(); // 일부 브라우저에서 시간 선택기 강제 실행
});
document.querySelector('input[id="tripDate"]').addEventListener('click', function() {
this.showPicker && this.showPicker(); // 일부 브라우저에서 시간 선택기 강제 실행
});
// 사용자 목록 로딩 (검색 포함)
function loadUserList(keyword = "") {
$.ajax({
url: '/api/admin/user/search/name'
,type: 'GET'
,data: { userName: keyword }
,success: function (resutl) {
console.log(' + loadUserList resutl : ', resutl);
const tbody = document.getElementById("userSearchResult");
tbody.innerHTML = ""; // 초기화
var data = resutl.data;
if (data && data.length > 0) {
data.forEach(user => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${user.userName}</td>
<td>${user.deptNm || "-"}</td>
<td>${user.userRank || "-"}</td>
<td>
<button class="btn btn-sm btn-success" onclick="selectUser('${user.userName}', '${user.deptNm}', '${user.mobilePhone}')">
<i class="fas fa-user-plus"></i> 추가
</button>
</td>
`;
tbody.appendChild(row);
});
} else {
tbody.innerHTML = `<tr><td colspan="5" class="text-center text-muted">검색 결과가 없습니다.</td></tr>`;
}
},
error: function () {
alert("사용자 목록을 불러오는 데 실패했습니다.");
}
});
}
// 모달 열릴 때 사용자 목록 자동 로드
$('#userSearchModal').on('shown.bs.modal', function () {
loadUserList(); // 최초 전체 목록 로드
});
function selectUser(name, dept, phone) {
console.log('phone: ', phone);
// 원하는 방식으로 테이블에 사용자 추가
const tbody = document.getElementById("tripMemberTbody");
const newRow = document.createElement("tr");
newRow.innerHTML = `
<td>${name}</td>
<td>${dept}</td>
<td>${phone}</td>
<td class="text-center">
<a href="#" class="btn btn-danger btn-sm" onclick="removeMemberRow(this)">
<i class="fas fa-user-minus"></i> 삭제
</a>
</td>
`;
tbody.appendChild(newRow);
// 모달 닫기
$('#userSearchModal').modal('hide');
}
// 검색 버튼 클릭 시
function searchUser() {
const keyword = document.getElementById("userSearchKeyword").value.trim();
loadUserList(keyword);
}
// 출장 인원 행 삭제 함수
function removeMemberRow(el) {
if (confirm("정말 삭제하시겠습니까?")) {
const row = el.closest("tr");
if (row) {
row.remove();
}
}
}
</script>
<script th:src="@{/cmn/js/bizTrip/init.js}"></script>
<script th:src="@{/cmn/js/bizTrip/event.js}"></script>
<script th:src="@{/cmn/js/bizTrip/service.js}"></script>
<script th:src="@{/cmn/js/bizTrip/validation.js}"></script>
</body>

View File

@ -161,13 +161,23 @@
<label for="userId">ID</label>
<input type="text" class="form-control" id="userId" name="userId" readonly>
</div>
<div class="form-group">
<label for="mobilePhone">핸드폰번호</label>
<input type="text" class="form-control" id="mobilePhone" name="mobilePhone">
</div>
<div class="form-group">
<label for="userName">이름</label>
<input type="text" class="form-control" id="userName" name="userName">
</div>
<div class="form-group">
<label for="userRank">직급</label>
<input type="text" class="form-control" id="userRank" name="userRank" placeholder="직급을 입력해 주세요.">
<label for="rankCd">직급</label>
<select class="form-control" id="rankCd" name="rankCd">
<option value="" th:selected>-- 선택 --</option>
<option th:each="code : ${@TCodeUtils.getCodeList('RANK')}"
th:value="${code.codeId}"
th:text="${code.codeName}">
</option>
</select>
</div>
<div class="form-group">
<label for="deptCd">부서명</label>
@ -328,8 +338,9 @@
var modalId = '#editUserInfoModal';
$(modalId+' #uniqId').val(dataInfo.uniqId);
$(modalId+' #userId').val(dataInfo.userId);
$(modalId+' #userName').val(dataInfo.username);
$(modalId+' #userRank').val(dataInfo.userRank);
$(modalId+' #userName').val(dataInfo.userName);
$(modalId+' #mobilePhone').val(dataInfo.mobilePhone);
$(modalId+' #rankCd').val(dataInfo.rankCd);
$(modalId+' #deptCd').val(dataInfo.deptCd);
$(modalId+' #hireDate').val(dataInfo.hireDate);
$(modalId+' #resignDate').val(dataInfo.resignDate);