출장등록 완료

This commit is contained in:
hehihoho3@gmail.com 2025-04-09 18:17:25 +09:00
parent 589a3cf456
commit a6093f9961
32 changed files with 1602 additions and 609 deletions

View File

@ -86,12 +86,13 @@ public class SecurityConfig {
.failureHandler(customAuthenticationFailureHandler()) // 로그인 실패 핸들러
.permitAll() // 로그인 페이지는 누구나 접근 가능
)
// 로그아웃 설정
.logout((logoutConfig) -> logoutConfig
.logoutUrl("/logout") // 로그아웃 요청 URL
.logoutSuccessUrl("/user/login") // 로그아웃 성공 리다이렉트 URL
.invalidateHttpSession(true) // 로그아웃 세션 무효화
.deleteCookies("JSESSIONID") // JSESSIONID 쿠키 삭제
// .deleteCookies("JSESSIONID") // JSESSIONID 쿠키 삭제
.permitAll() // 로그아웃은 누구나 요청 가능
)
// 세션 관리 설정
@ -122,15 +123,31 @@ public class SecurityConfig {
// 인증된 사용자 정보를 가져옴
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
// session 설정
// 보안이슈로인해 필요한 데이터만
UserVO loginVO = new UserVO();
loginVO.setUserName( userDetails.getUser().getUserName());
request.getSession().setAttribute("loginVO", loginVO);
// 요청 URL의 호스트를 추출 (디버깅용)
String host_url = new URL(request.getRequestURL().toString()).getHost();
System.out.println("host_url : " + host_url);
// System.out.println("host_url : " + host_url);
// 로컬 환경이 아닌 경우 로그인 로그를 기록 (2024-05-22 수정)
if (!"localhost".equals(host_url)) {
String id = userDetails.getId();
userService.loginLog(id); // 로그인 기록 저장
}
//
// var rememberMeServices = new org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices(
// "secureAndRandomKey", customUserDetailsService
// );
// rememberMeServices.setAlwaysRemember(true);
// rememberMeServices.onLoginSuccess(request, response, authentication);
response.setStatus(HttpStatus.OK.value()); // HTTP 상태 코드 200 설정
response.sendRedirect("/"); // 루트 경로로 리다이렉트
};

View File

@ -1,11 +1,11 @@
package com.itn.admin.cmn.util.thymeleafUtils;
import com.itn.admin.itn.code.mapper.domain.CodeDetailVO;
import com.itn.admin.itn.code.mapper.domain.CodeVO;
import com.itn.admin.itn.code.server.CodeDetailService;
import com.itn.admin.itn.user.mapper.UserMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thymeleaf.util.StringUtils;
import java.util.List;
@ -16,6 +16,8 @@ public class TCodeUtils {
@Autowired
private CodeDetailService codeDetailService;
@Autowired
private UserMapper userMapper;
public String getCodeName(String codeGroupId, String codeValue) {
if(StringUtils.isEmpty(codeValue)){
@ -23,9 +25,42 @@ public class TCodeUtils {
};
return codeDetailService.getCodeName(codeGroupId, codeValue);
}
public List<CodeDetailVO> getCodeList(String codeGroupId) {
return codeDetailService.getDetailsByGroupId(codeGroupId);
}
public String getUserName(String uniqId) {
if(StringUtils.isEmpty(uniqId)){
return uniqId;
}
String userName = userMapper.findById(uniqId).getUserName();
return StringUtils.isEmpty(userName) ? uniqId : userName ;
}
public String getUserDeptTxt(String uniqId) {
String deptCd = userMapper.findById(uniqId).getDeptCd();
String dept = "";
if(StringUtils.isNotEmpty(deptCd)){
dept = codeDetailService.getCodeName("DEPT", deptCd);
}
return StringUtils.isEmpty(dept) ? "-" : dept ;
}
public String getUserRankTxt(String uniqId) {
String rankCd = userMapper.findById(uniqId).getRankCd();
String rank = "";
if(StringUtils.isNotEmpty(rankCd)){
rank = codeDetailService.getCodeName("RANK", rankCd);
}
return StringUtils.isEmpty(rank) ? "-" : rank ;
}
public String getUserMobilePhone(String uniqId) {
String mobilePhone = userMapper.findById(uniqId).getMobilePhone();
return StringUtils.isEmpty(mobilePhone) ? uniqId : mobilePhone ;
}
}

View File

@ -1,17 +1,45 @@
package com.itn.admin.init.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 jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
@Slf4j
@Controller
public class DashboardController {
@Autowired
private BizTripService bizTripService;
@GetMapping("/")
public String index() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println("authentication.getAuthorities(); : "+ authentication.getAuthorities());
public String index(HttpServletRequest request
, Model model
, @AuthenticationPrincipal CustomUserDetails loginUser) {
List<BizTripVO> myApprovalslist = bizTripService.getMyPendingApprovals(loginUser.getUser().getUniqId());
model.addAttribute("myApprovalslist", myApprovalslist);
// Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// System.out.println("authentication.getAuthorities(); : "+ authentication.getAuthorities());
// HttpSession session = request.getSession();
// Object loginVO = session.getAttribute("loginVO");
// log.info("세션에 저장된 사용자 정보: {}", loginVO instanceof UserVO);
// log.info("세션에 저장된 사용자 정보: {}", loginVO);
return "dashboard/index";
}

View File

@ -1,9 +1,11 @@
package com.itn.admin.itn.bizTrip.mapper;
import com.itn.admin.cmn.msg.RestResponse;
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 org.apache.ibatis.annotations.Select;
import java.util.List;
@ -16,4 +18,20 @@ public interface BizTripMapper {
void insertApprovalLine(BizTripApprovalVO approval);
List<BizTripVO> selectTripList();
BizTripVO getBizTripWithDetail(String tripId);
List<BizTripVO> getMyPendingApprovals(String uniqId);
RestResponse saveApproval(BizTripApprovalVO bizTripApprovalVO);
void updateTripStatus(BizTripVO bizTripVO);
String getNextApproverId(int tripId, String approverId);
void updateBizTrip(BizTripVO trip);
void deleteTripMembers(Integer tripId);
void deleteApprovalLines(Integer tripId);
}

View File

@ -7,6 +7,7 @@ import lombok.experimental.SuperBuilder;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Date;
@Getter
@ -22,7 +23,7 @@ public class BizTripApprovalVO extends CmnVO {
private String approverId; // 결재자 uniq_id
private Integer orderNo; // 결재 순서
private String approveStatus; // 결재 상태 (WAIT, APPROVED, REJECTED)
private LocalDateTime approveDt; // 결재 일시
private Date approveDt; // 결재 일시
private String comment; // 결재 의견

View File

@ -6,6 +6,7 @@ import lombok.experimental.SuperBuilder;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;
@Getter
@ -26,8 +27,14 @@ public class BizTripVO extends CmnVO {
private LocalTime startTime; // 출장 시작시간
private LocalTime endTime; // 출장 종료시간
private String status; // 결재 상태 (ING, DONE )
private String useYn; // 사용여부
private String latestApproveStatus; // 최신 결재 상태
private String currentApproverId; // 현재 결제 대기자
private List<BizTripMemberVO> memberList;
private List<BizTripApprovalVO> approvalList;
}

View File

@ -1,13 +1,26 @@
package com.itn.admin.itn.bizTrip.service;
import com.itn.admin.cmn.msg.RestResponse;
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripApprovalVO;
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripRequestDTO;
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripVO;
import com.itn.admin.itn.user.mapper.domain.UserVO;
import java.util.List;
import java.util.Map;
public interface BizTripService {
RestResponse register(BizTripRequestDTO dto);
List<BizTripVO> selectTripList();
Map<String, Object> getBizTripWithDetail(String tripId);
List<BizTripVO> getMyPendingApprovals(String uniqId);
RestResponse approval(BizTripApprovalVO bizTripApprovalVO, UserVO userVO);
Map<String, Object> getBizTripWithEdit(String tripId);
RestResponse update(BizTripRequestDTO dto);
}

View File

@ -10,11 +10,14 @@ 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 com.itn.admin.itn.user.mapper.domain.UserVO;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.*;
@Service
public class BizTripServiceImpl implements BizTripService {
@ -49,12 +52,146 @@ public class BizTripServiceImpl implements BizTripService {
}
return new RestResponse(HttpStatus.OK, "등록되었습니다");
return new RestResponse(HttpStatus.OK, "등록되었습니다",tripId);
}
@Override
public RestResponse update(BizTripRequestDTO dto) {
BizTripVO trip = dto.getTripInfo();
Integer tripId = trip.getTripId();
// 1. 출장 기본 정보 수정
bizTripMapper.updateBizTrip(trip); // tripId 포함되어 있어야
// 2. 기존 출장 인원 삭제 재등록
bizTripMapper.deleteTripMembers(tripId);
List<BizTripMemberVO> memberList = dto.getTripMembers();
if (memberList != null && !memberList.isEmpty()) {
for (BizTripMemberVO member : memberList) {
member.setTripId(tripId);
bizTripMapper.insertTripMember(member);
}
}
// 3. 기존 결재라인 삭제 재등록
bizTripMapper.deleteApprovalLines(tripId);
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();
}
@Override
public Map<String, Object> getBizTripWithDetail(String tripId) {
Map<String, Object> returnMap = new HashMap<>();
BizTripVO bizTrip = bizTripMapper.getBizTripWithDetail(tripId);
// 상세화면
String firstWaitingApproverId = bizTrip.getApprovalList().stream()
.filter(a -> "10".equals(a.getApproveStatus()))
.map(BizTripApprovalVO::getApproverId)
.findFirst()
.orElse(null);
/*더 필요한 데이터있으면 추가해서 returnMap에 put 해서 넘겨주기*/
returnMap.put("bizTrip", bizTrip);
returnMap.put("firstWaitingApproverId", firstWaitingApproverId);
return returnMap;
}
@Override
public Map<String, Object> getBizTripWithEdit(String tripId) {
Map<String, Object> returnMap = new HashMap<>();
BizTripVO bizTrip = bizTripMapper.getBizTripWithDetail(tripId);
// 검토1, 검토2, 결제를 구현하기 위한 로직
List<BizTripApprovalVO> finalList = new ArrayList<>();
for (int i = 1; i <= 3; i++) {
// 복사해서 final 변수로 만듦
// 불변이라고 선언을 해야 람다에서 사용가능
final int order = i;
Optional<BizTripApprovalVO> found = bizTrip.getApprovalList().stream()
.filter(vo -> vo.getOrderNo() == order)
.findFirst();
if (found.isPresent()) {
finalList.add(found.get());
} else {
BizTripApprovalVO empty = new BizTripApprovalVO();
empty.setOrderNo(i);
finalList.add(empty);
}
}
bizTrip.setApprovalList(finalList);
// -- 검토1, 검토2, 결제를 구현하기 위한 로직
/*더 필요한 데이터있으면 추가해서 returnMap에 put 해서 넘겨주기*/
returnMap.put("bizTrip", bizTrip);
return returnMap;
}
@Override
public List<BizTripVO> getMyPendingApprovals(String uniqId) {
return bizTripMapper.getMyPendingApprovals(uniqId);
}
@Override
@Transactional
public RestResponse approval(BizTripApprovalVO bizTripApprovalVO, UserVO userVO) {
// '결재 상태(APPR_STS - 대기:10, 승인:30, 거절:40)'
String approveStatus = bizTripApprovalVO.getApproveStatus();
int tripId = bizTripApprovalVO.getTripId();
bizTripMapper.saveApproval(bizTripApprovalVO);
// 다음 결제자 있는지 확인하는 로직
// 있으면 uniqId 가져옴
String nextApproverId = bizTripMapper.getNextApproverId(tripId, bizTripApprovalVO.getApproverId());
BizTripVO bizTripVO = BizTripVO.builder()
.tripId(tripId)
.status(approveStatus)
.lastUpdusrId(userVO.getUniqId())
.build();
if (StringUtils.isNotEmpty(nextApproverId) && "30".equals(approveStatus)) {
bizTripVO.setStatus("20");
// 다음 결재자 있음 & 현재 승인 '진행중'
bizTripMapper.updateTripStatus(bizTripVO);
//TODO [slack] nextApproverId를 참고해서 알려줘야함
} else {
// 마지막 결재자 or 반려 본인 상태 그대로 반영
bizTripMapper.updateTripStatus(bizTripVO);
//TODO [slack] 결제 완료되었다는걸 알려줘야함
}
return new RestResponse(HttpStatus.OK,"결제가 완료되었습니다.");
}
}

View File

@ -1,6 +1,7 @@
package com.itn.admin.itn.bizTrip.web;
import com.itn.admin.cmn.config.CustomUserDetails;
import com.itn.admin.itn.bizTrip.mapper.domain.BizTripMemberVO;
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;
@ -12,8 +13,11 @@ 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 org.springframework.web.bind.annotation.PathVariable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Controller
@ -32,11 +36,43 @@ public class BizTripController {
return "itn/bizTrip/reg";
}
@GetMapping("/itn/bizTrip/edit/{tripId}")
public String edit(@PathVariable String tripId
, @AuthenticationPrincipal CustomUserDetails loginUser
,Model model
) {
Map<String, Object> returnMap = bizTripService.getBizTripWithEdit(tripId);
model.addAttribute("trip", returnMap.get("bizTrip"));
model.addAttribute("loginUser", loginUser.getUser());
return "itn/bizTrip/edit";
}
@GetMapping("/itn/bizTrip/list")
public String bizTripList(Model model) {
public String bizTripList(Model model
,@AuthenticationPrincipal CustomUserDetails loginUser) {
List<BizTripVO> list = bizTripService.selectTripList();
model.addAttribute("list", list);
for (BizTripVO vo : list) {
log.info(" + vo :: [{}]", vo.getCurrentApproverId());
}
model.addAttribute("loginUser", loginUser.getUser());
log.info(" + loginUser :: [{}]", loginUser.getUser());
return "itn/bizTrip/list"; // Thymeleaf HTML 파일 경로
}
@GetMapping("/itn/bizTrip/detail/{tripId}")
public String tripDetail(@PathVariable String tripId
,@AuthenticationPrincipal CustomUserDetails loginUser
, Model model
) {
Map<String, Object> returnMap = bizTripService.getBizTripWithDetail(tripId);
model.addAttribute("trip", returnMap.get("bizTrip"));
model.addAttribute("loginUser", loginUser.getUser());
model.addAttribute("firstWaitingApproverId", returnMap.get("firstWaitingApproverId"));
return "itn/bizTrip/tripDetail";
}
}

View File

@ -2,6 +2,7 @@ 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.BizTripApprovalVO;
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;
@ -27,11 +28,24 @@ public class BizTripRestController {
, @AuthenticationPrincipal CustomUserDetails loginUser) {
dto.getTripInfo().setFrstRegisterId(loginUser.getUser().getUniqId());
log.info("dto: [{}]", dto);
// bizTripService.register(dto);
return ResponseEntity.ok().body(bizTripService.register(dto));
}
@PostMapping("/api/bizTrip/approval")
public ResponseEntity<RestResponse> approval(@RequestBody BizTripApprovalVO bizTripApprovalVO
, @AuthenticationPrincipal CustomUserDetails loginUser) {
log.info("bizTripApprovalVO: [{}]", bizTripApprovalVO);
// bizTripService.register(dto);
return ResponseEntity.ok().body(bizTripService.approval(bizTripApprovalVO, loginUser.getUser()));
}
@PutMapping("/api/bizTrip/update")
public ResponseEntity<RestResponse> updateBizTrip(@RequestBody BizTripRequestDTO dto,
@AuthenticationPrincipal CustomUserDetails loginUser) {
dto.getTripInfo().setLastUpdusrId(loginUser.getUser().getUniqId());
return ResponseEntity.ok().body(bizTripService.update(dto));
}
}

View File

@ -38,12 +38,13 @@ public class UserRestController {
}
// 특정 코드 그룹을 ID로 가져오는 메서드
//
@GetMapping("/api/admin/user/{uniqId}")
public ResponseEntity<?> findByUniqId(@PathVariable String uniqId) {
return ResponseEntity.ok(userService.findByUniqId(uniqId));
}
// 특정 코드 그룹을 ID로 가져오는 메서드
//
@GetMapping("/api/admin/user/search/name")
public ResponseEntity<?> findByUniqUserName(@RequestParam String userName) {
log.info("userName: {}", userName);

View File

@ -67,40 +67,118 @@
ELSE '10' /* 전부 대기 */
END AS status
/* 현재 결재 대기자 추가 */
,(
SELECT a.approver_id
FROM biz_trip_approval a
WHERE a.trip_id = bt.trip_id
AND a.approve_status = '10'
ORDER BY a.order_no ASC
LIMIT 1
) AS current_approver_id
FROM biz_trip bt
WHERE bt.use_yn = 'Y'
ORDER BY bt.trip_dt DESC
]]>
</select>
<!--
&lt;!&ndash; 모든 코드 그룹을 조회하는 쿼리 &ndash;&gt;
<select id="findAll" resultType="codeVO">
SELECT * FROM common_code
<!-- 출장 상세조회 + 하위 정보까지 포함하는 resultMap 정의 -->
<resultMap id="tripDetailMap" type="bizTripVO">
<!-- 출장 ID를 기준으로 기본 정보 바인딩 -->
<id property="tripId" column="trip_id"/>
<!-- 나머지 속성들은 mapUnderscoreToCamelCase 설정으로 자동 매핑됨 -->
<!-- 출장 인원 목록: trip_id를 기준으로 getTripMembers 쿼리 실행 -->
<collection property="memberList" ofType="bizTripMemberVO" column="trip_id" select="getTripMembers"/>
<!-- 결재 라인 목록: trip_id를 기준으로 getTripApprovals 쿼리 실행 -->
<collection property="approvalList" ofType="bizTripApprovalVO" column="trip_id" select="getTripApprovals"/>
</resultMap>
<!-- 출장 기본 정보 단건 조회 -->
<select id="getBizTripWithDetail" resultMap="tripDetailMap">
SELECT * FROM biz_trip WHERE trip_id = #{tripId}
</select>
&lt;!&ndash; 특정 코드 그룹을 ID로 조회하는 쿼리 &ndash;&gt;
<select id="findById" parameterType="String" resultType="codeVO">
SELECT * FROM common_code WHERE code_group_id = #{codeGroupId}
<!-- 출장 인원 목록 조회 -->
<select id="getTripMembers" resultType="bizTripMemberVO">
SELECT * FROM biz_trip_member WHERE trip_id = #{tripId}
</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>
<!-- 출장 결재 라인 목록 조회 -->
<select id="getTripApprovals" resultType="bizTripApprovalVO">
SELECT * FROM biz_trip_approval WHERE trip_id = #{tripId}
</select>
&lt;!&ndash; 코드 그룹을 수정하는 쿼리 &ndash;&gt;
<update id="update" parameterType="codeVO">
UPDATE common_code
SET code_group_name = #{codeGroupName},
description = #{description},
<select id="getMyPendingApprovals" resultType="bizTripVO">
SELECT bt.*
FROM biz_trip bt
JOIN biz_trip_approval bta ON bt.trip_id = bta.trip_id
WHERE bta.approver_id = #{uniqId}
AND bta.approve_status = '10'
AND bta.order_no = (
SELECT MIN(bta2.order_no)
FROM biz_trip_approval bta2
WHERE bta2.trip_id = bt.trip_id
AND bta2.approve_status = '10'
)
ORDER BY bt.trip_dt DESC
</select>
<select id="saveApproval" resultType="bizTripApprovalVO">
UPDATE biz_trip_approval
SET approve_status = #{approveStatus},
approve_dt = NOW()
WHERE id = #{id}
AND approver_id = #{approverId}
</select>
<update id="updateTripStatus">
UPDATE biz_trip
SET status = #{status},
last_updusr_id = #{lastUpdusrId},
last_updt_pnttm = #{lastUpdtPnttm}
WHERE code_group_id = #{codeGroupId}
last_updt_pnttm = NOW()
WHERE trip_id = #{tripId}
</update>
&lt;!&ndash; 코드 그룹을 삭제하는 쿼리 &ndash;&gt;
<delete id="delete" parameterType="String">
DELETE FROM common_code WHERE code_group_id = #{codeGroupId}
</delete>-->
<select id="getNextApproverId" resultType="string">
SELECT approver_id
FROM biz_trip_approval
WHERE trip_id = #{tripId}
AND order_no > (
SELECT order_no
FROM biz_trip_approval
WHERE trip_id = #{tripId}
AND approver_id = #{approverId}
)
ORDER BY order_no
LIMIT 1
</select>
<update id="updateBizTrip" parameterType="bizTripVO">
UPDATE biz_trip
SET
trip_type_cd = #{tripTypeCd},
location_cd = #{locationCd},
location_txt = #{locationTxt},
purpose = #{purpose},
move_cd = #{moveCd},
trip_dt = #{tripDt},
start_time = #{startTime},
end_time = #{endTime},
status = #{status},
last_updusr_id = #{lastUpdusrId},
last_updt_pnttm = NOW()
WHERE trip_id = #{tripId}
</update>
<delete id="deleteTripMembers" parameterType="int">
DELETE FROM biz_trip_member
WHERE trip_id = #{tripId}
</delete>
<delete id="deleteApprovalLines" parameterType="int">
DELETE FROM biz_trip_approval
WHERE trip_id = #{tripId}
</delete>
</mapper>

View File

@ -54,7 +54,7 @@
,ic.holi_code
-- ,ic.pstn
,us.user_name
,us.user_rank
,us.rank_cd
from itn_commute ic
left join itn_commute_group icg
on ic.commute_group_id = icg.commute_group_id

View File

@ -0,0 +1,74 @@
/**
* @discription 수정 액션
* @param tripId
*/
function updateTripData(tripId) {
if (!validateTripForm()) return;
// 값 생성
const tripInfo = {
tripId: tripId,
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("update payload:", payload);
// Ajax 전송
$.ajax({
url: '/api/bizTrip/update',
method: 'PUT',
contentType: 'application/json',
data: JSON.stringify(payload),
success: function(data) {
console.log('data : ', data);
// Toast 먼저 띄움 (바로 표시됨)
// fn_successAlert("수정 성공", data.msg);
Swal.fire({
title: data.msg,
text: '목록으로 이동하시겠습니까?',
icon: 'success',
showCancelButton: true,
confirmButtonText: '이동',
width: 300,
cancelButtonText: '취소'
}).then((result) => {
if (result.isConfirmed) {
location.href = "/itn/bizTrip/list";
} else if (result.isDismissed) {
location.reload();
}
});
},
error: () => fn_failedAlert("수정 실패", "오류가 발생했습니다.")
});
}

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

@ -46,7 +46,25 @@ function collectAndSubmitTripData() {
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(payload),
success: () => fn_successAlert("등록 성공", "출장 정보가 저장되었습니다."),
success: function(data) {
Swal.fire({
title: "출장 정보가 저장되었습니다.",
text: '상세페이지로 이동하시겠습니까?',
icon: 'success',
showCancelButton: true,
confirmButtonText: '상세',
cancelButtonText: '목록'
}).then((result) => {
if (result.isConfirmed) {
location.href = "/itn/bizTrip/detail/"+data.data;
} else if (result.isDismissed) {
location.href = "/itn/bizTrip/list";
}
});
},
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

@ -6,8 +6,10 @@
layout:decorate="layout">
<head>
<!-- layout.html 에 들어간 head 부분을 제외하고 개별 파일에만 적용되는 head 부분 추가 -->
<title>401</title>
<th:block layout:fragment="title">
<title>401</title>
</th:block>
<!-- 필요하다면 개별 파일에 사용될 css/js 선언 -->
<link rel="stylesheet" th:href="@{/plugins/datatables-bs4/css/dataTables.bootstrap4.min.css}">

View File

@ -111,512 +111,52 @@
<!-- Main row -->
<div class="row">
<!-- Left col -->
<section class="col-lg-7 connectedSortable">
<!-- Custom tabs (Charts with tabs)-->
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-chart-pie mr-1"></i>
Sales
</h3>
<div class="card-tools">
<ul class="nav nav-pills ml-auto">
<li class="nav-item">
<a class="nav-link active" href="#revenue-chart" data-toggle="tab">Area</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#sales-chart" data-toggle="tab">Donut</a>
</li>
</ul>
<section class="col-lg-6 connectedSortable">
<div class="col-12">
<div class="card border-0 shadow rounded-lg mb-4">
<div class="card-header bg-primary text-white rounded-top">
<h5 class="mb-0">
<i class="fas fa-clipboard-check mr-2"></i>
내 결재 대기 출장
</h5>
</div>
</div><!-- /.card-header -->
<div class="card-body">
<div class="tab-content p-0">
<!-- Morris chart - Sales -->
<div class="chart tab-pane active" id="revenue-chart"
style="position: relative; height: 300px;">
<canvas id="revenue-chart-canvas" height="300" style="height: 300px;"></canvas>
</div>
<div class="chart tab-pane" id="sales-chart" style="position: relative; height: 300px;">
<canvas id="sales-chart-canvas" height="300" style="height: 300px;"></canvas>
</div>
<div class="card-body p-0">
<table class="table table-bordered mb-0 text-center" style="border-radius: 0 0 .5rem .5rem; overflow: hidden;">
<thead class="thead-light">
<tr>
<th>출장일자</th>
<th>목적지</th>
<th>목적</th>
<th>상태</th>
</tr>
</thead>
<tbody>
<tr th:if="${#lists.isEmpty(myApprovalslist)}">
<td colspan="4" class="text-muted">결재할 출장 없음</td>
</tr>
<tr th:each="row : ${myApprovalslist}"
th:onclick="|location.href='@{/itn/bizTrip/detail/{tripId}(tripId=${row.tripId})}'|"
class="cursor-pointer">
<td th:text="${row.tripDt}"></td>
<td th:text="${row.locationTxt}"></td>
<td th:text="${row.purpose}"></td>
<td>
<span class="badge"
th:classappend="${row.status == '10'} ? ' badge-warning' :
(${row.status == '20'} ? ' badge-info' :
(${row.status == '30'} ? ' badge-success' : ' badge-danger'))"
th:text="${@TCodeUtils.getCodeName('APPR_STS', row.status)}">
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div><!-- /.card-body -->
</div>
<!-- /.card -->
<!-- DIRECT CHAT -->
<div class="card direct-chat direct-chat-primary">
<div class="card-header">
<h3 class="card-title">Direct Chat</h3>
<div class="card-tools">
<span title="3 New Messages" class="badge badge-primary">3</span>
<button type="button" class="btn btn-tool" data-card-widget="collapse">
<i class="fas fa-minus"></i>
</button>
<button type="button" class="btn btn-tool" title="Contacts" data-widget="chat-pane-toggle">
<i class="fas fa-comments"></i>
</button>
<button type="button" class="btn btn-tool" data-card-widget="remove">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<!-- /.card-header -->
<div class="card-body">
<!-- Conversations are loaded here -->
<div class="direct-chat-messages">
<!-- Message. Default to the left -->
<div class="direct-chat-msg">
<div class="direct-chat-infos clearfix">
<span class="direct-chat-name float-left">Alexander Pierce</span>
<span class="direct-chat-timestamp float-right">23 Jan 2:00 pm</span>
</div>
<!-- /.direct-chat-infos -->
<img class="direct-chat-img" src="dist/img/user1-128x128.jpg" alt="message user image">
<!-- /.direct-chat-img -->
<div class="direct-chat-text">
Is this template really for free? That's unbelievable!
</div>
<!-- /.direct-chat-text -->
</div>
<!-- /.direct-chat-msg -->
<!-- Message to the right -->
<div class="direct-chat-msg right">
<div class="direct-chat-infos clearfix">
<span class="direct-chat-name float-right">Sarah Bullock</span>
<span class="direct-chat-timestamp float-left">23 Jan 2:05 pm</span>
</div>
<!-- /.direct-chat-infos -->
<img class="direct-chat-img" src="dist/img/user3-128x128.jpg" alt="message user image">
<!-- /.direct-chat-img -->
<div class="direct-chat-text">
You better believe it!
</div>
<!-- /.direct-chat-text -->
</div>
<!-- /.direct-chat-msg -->
<!-- Message. Default to the left -->
<div class="direct-chat-msg">
<div class="direct-chat-infos clearfix">
<span class="direct-chat-name float-left">Alexander Pierce</span>
<span class="direct-chat-timestamp float-right">23 Jan 5:37 pm</span>
</div>
<!-- /.direct-chat-infos -->
<img class="direct-chat-img" src="dist/img/user1-128x128.jpg" alt="message user image">
<!-- /.direct-chat-img -->
<div class="direct-chat-text">
Working with AdminLTE on a great new app! Wanna join?
</div>
<!-- /.direct-chat-text -->
</div>
<!-- /.direct-chat-msg -->
<!-- Message to the right -->
<div class="direct-chat-msg right">
<div class="direct-chat-infos clearfix">
<span class="direct-chat-name float-right">Sarah Bullock</span>
<span class="direct-chat-timestamp float-left">23 Jan 6:10 pm</span>
</div>
<!-- /.direct-chat-infos -->
<img class="direct-chat-img" src="dist/img/user3-128x128.jpg" alt="message user image">
<!-- /.direct-chat-img -->
<div class="direct-chat-text">
I would love to.
</div>
<!-- /.direct-chat-text -->
</div>
<!-- /.direct-chat-msg -->
</div>
<!--/.direct-chat-messages-->
<!-- Contacts are loaded here -->
<div class="direct-chat-contacts">
<ul class="contacts-list">
<li>
<a href="#">
<img class="contacts-list-img" src="dist/img/user1-128x128.jpg" alt="User Avatar">
<div class="contacts-list-info">
<span class="contacts-list-name">
Count Dracula
<small class="contacts-list-date float-right">2/28/2015</small>
</span>
<span class="contacts-list-msg">How have you been? I was...</span>
</div>
<!-- /.contacts-list-info -->
</a>
</li>
<!-- End Contact Item -->
<li>
<a href="#">
<img class="contacts-list-img" src="dist/img/user7-128x128.jpg" alt="User Avatar">
<div class="contacts-list-info">
<span class="contacts-list-name">
Sarah Doe
<small class="contacts-list-date float-right">2/23/2015</small>
</span>
<span class="contacts-list-msg">I will be waiting for...</span>
</div>
<!-- /.contacts-list-info -->
</a>
</li>
<!-- End Contact Item -->
<li>
<a href="#">
<img class="contacts-list-img" src="dist/img/user3-128x128.jpg" alt="User Avatar">
<div class="contacts-list-info">
<span class="contacts-list-name">
Nadia Jolie
<small class="contacts-list-date float-right">2/20/2015</small>
</span>
<span class="contacts-list-msg">I'll call you back at...</span>
</div>
<!-- /.contacts-list-info -->
</a>
</li>
<!-- End Contact Item -->
<li>
<a href="#">
<img class="contacts-list-img" src="dist/img/user5-128x128.jpg" alt="User Avatar">
<div class="contacts-list-info">
<span class="contacts-list-name">
Nora S. Vans
<small class="contacts-list-date float-right">2/10/2015</small>
</span>
<span class="contacts-list-msg">Where is your new...</span>
</div>
<!-- /.contacts-list-info -->
</a>
</li>
<!-- End Contact Item -->
<li>
<a href="#">
<img class="contacts-list-img" src="dist/img/user6-128x128.jpg" alt="User Avatar">
<div class="contacts-list-info">
<span class="contacts-list-name">
John K.
<small class="contacts-list-date float-right">1/27/2015</small>
</span>
<span class="contacts-list-msg">Can I take a look at...</span>
</div>
<!-- /.contacts-list-info -->
</a>
</li>
<!-- End Contact Item -->
<li>
<a href="#">
<img class="contacts-list-img" src="dist/img/user8-128x128.jpg" alt="User Avatar">
<div class="contacts-list-info">
<span class="contacts-list-name">
Kenneth M.
<small class="contacts-list-date float-right">1/4/2015</small>
</span>
<span class="contacts-list-msg">Never mind I found...</span>
</div>
<!-- /.contacts-list-info -->
</a>
</li>
<!-- End Contact Item -->
</ul>
<!-- /.contacts-list -->
</div>
<!-- /.direct-chat-pane -->
</div>
<!-- /.card-body -->
<div class="card-footer">
<form action="#" method="post">
<div class="input-group">
<input type="text" name="message" placeholder="Type Message ..." class="form-control">
<span class="input-group-append">
<button type="button" class="btn btn-primary">Send</button>
</span>
</div>
</form>
</div>
<!-- /.card-footer-->
</div>
<!--/.direct-chat -->
<!-- TO DO List -->
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="ion ion-clipboard mr-1"></i>
To Do List
</h3>
<div class="card-tools">
<ul class="pagination pagination-sm">
<li class="page-item"><a href="#" class="page-link">&laquo;</a></li>
<li class="page-item"><a href="#" class="page-link">1</a></li>
<li class="page-item"><a href="#" class="page-link">2</a></li>
<li class="page-item"><a href="#" class="page-link">3</a></li>
<li class="page-item"><a href="#" class="page-link">&raquo;</a></li>
</ul>
</div>
</div>
<!-- /.card-header -->
<div class="card-body">
<ul class="todo-list" data-widget="todo-list">
<li>
<!-- drag handle -->
<span class="handle">
<i class="fas fa-ellipsis-v"></i>
<i class="fas fa-ellipsis-v"></i>
</span>
<!-- checkbox -->
<div class="icheck-primary d-inline ml-2">
<input type="checkbox" value="" name="todo1" id="todoCheck1">
<label for="todoCheck1"></label>
</div>
<!-- todo text -->
<span class="text">Design a nice theme</span>
<!-- Emphasis label -->
<small class="badge badge-danger"><i class="far fa-clock"></i> 2 mins</small>
<!-- General tools such as edit or delete-->
<div class="tools">
<i class="fas fa-edit"></i>
<i class="fas fa-trash-o"></i>
</div>
</li>
<li>
<span class="handle">
<i class="fas fa-ellipsis-v"></i>
<i class="fas fa-ellipsis-v"></i>
</span>
<div class="icheck-primary d-inline ml-2">
<input type="checkbox" value="" name="todo2" id="todoCheck2" checked>
<label for="todoCheck2"></label>
</div>
<span class="text">Make the theme responsive</span>
<small class="badge badge-info"><i class="far fa-clock"></i> 4 hours</small>
<div class="tools">
<i class="fas fa-edit"></i>
<i class="fas fa-trash-o"></i>
</div>
</li>
<li>
<span class="handle">
<i class="fas fa-ellipsis-v"></i>
<i class="fas fa-ellipsis-v"></i>
</span>
<div class="icheck-primary d-inline ml-2">
<input type="checkbox" value="" name="todo3" id="todoCheck3">
<label for="todoCheck3"></label>
</div>
<span class="text">Let theme shine like a star</span>
<small class="badge badge-warning"><i class="far fa-clock"></i> 1 day</small>
<div class="tools">
<i class="fas fa-edit"></i>
<i class="fas fa-trash-o"></i>
</div>
</li>
<li>
<span class="handle">
<i class="fas fa-ellipsis-v"></i>
<i class="fas fa-ellipsis-v"></i>
</span>
<div class="icheck-primary d-inline ml-2">
<input type="checkbox" value="" name="todo4" id="todoCheck4">
<label for="todoCheck4"></label>
</div>
<span class="text">Let theme shine like a star</span>
<small class="badge badge-success"><i class="far fa-clock"></i> 3 days</small>
<div class="tools">
<i class="fas fa-edit"></i>
<i class="fas fa-trash-o"></i>
</div>
</li>
<li>
<span class="handle">
<i class="fas fa-ellipsis-v"></i>
<i class="fas fa-ellipsis-v"></i>
</span>
<div class="icheck-primary d-inline ml-2">
<input type="checkbox" value="" name="todo5" id="todoCheck5">
<label for="todoCheck5"></label>
</div>
<span class="text">Check your messages and notifications</span>
<small class="badge badge-primary"><i class="far fa-clock"></i> 1 week</small>
<div class="tools">
<i class="fas fa-edit"></i>
<i class="fas fa-trash-o"></i>
</div>
</li>
<li>
<span class="handle">
<i class="fas fa-ellipsis-v"></i>
<i class="fas fa-ellipsis-v"></i>
</span>
<div class="icheck-primary d-inline ml-2">
<input type="checkbox" value="" name="todo6" id="todoCheck6">
<label for="todoCheck6"></label>
</div>
<span class="text">Let theme shine like a star</span>
<small class="badge badge-secondary"><i class="far fa-clock"></i> 1 month</small>
<div class="tools">
<i class="fas fa-edit"></i>
<i class="fas fa-trash-o"></i>
</div>
</li>
</ul>
</div>
<!-- /.card-body -->
<div class="card-footer clearfix">
<button type="button" class="btn btn-primary float-right"><i class="fas fa-plus"></i> Add item</button>
</div>
</div>
<!-- /.card -->
</section>
<!-- /.Left col -->
<!-- right col (We are only adding the ID to make the widgets sortable)-->
<section class="col-lg-5 connectedSortable">
<!-- Map card -->
<div class="card bg-gradient-primary">
<div class="card-header border-0">
<h3 class="card-title">
<i class="fas fa-map-marker-alt mr-1"></i>
Visitors
</h3>
<!-- card tools -->
<div class="card-tools">
<button type="button" class="btn btn-primary btn-sm daterange" title="Date range">
<i class="far fa-calendar-alt"></i>
</button>
<button type="button" class="btn btn-primary btn-sm" data-card-widget="collapse" title="Collapse">
<i class="fas fa-minus"></i>
</button>
</div>
<!-- /.card-tools -->
</div>
<div class="card-body">
<div id="world-map" style="height: 250px; width: 100%;"></div>
</div>
<!-- /.card-body-->
<div class="card-footer bg-transparent">
<div class="row">
<div class="col-4 text-center">
<div id="sparkline-1"></div>
<div class="text-white">Visitors</div>
</div>
<!-- ./col -->
<div class="col-4 text-center">
<div id="sparkline-2"></div>
<div class="text-white">Online</div>
</div>
<!-- ./col -->
<div class="col-4 text-center">
<div id="sparkline-3"></div>
<div class="text-white">Sales</div>
</div>
<!-- ./col -->
</div>
<!-- /.row -->
</div>
</div>
<!-- /.card -->
<!-- solid sales graph -->
<div class="card bg-gradient-info">
<div class="card-header border-0">
<h3 class="card-title">
<i class="fas fa-th mr-1"></i>
Sales Graph
</h3>
<div class="card-tools">
<button type="button" class="btn bg-info btn-sm" data-card-widget="collapse">
<i class="fas fa-minus"></i>
</button>
<button type="button" class="btn bg-info btn-sm" data-card-widget="remove">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div class="card-body">
<canvas class="chart" id="line-chart" style="min-height: 250px; height: 250px; max-height: 250px; max-width: 100%;"></canvas>
</div>
<!-- /.card-body -->
<div class="card-footer bg-transparent">
<div class="row">
<div class="col-4 text-center">
<input type="text" class="knob" data-readonly="true" value="20" data-width="60" data-height="60"
data-fgColor="#39CCCC">
<div class="text-white">Mail-Orders</div>
</div>
<!-- ./col -->
<div class="col-4 text-center">
<input type="text" class="knob" data-readonly="true" value="50" data-width="60" data-height="60"
data-fgColor="#39CCCC">
<div class="text-white">Online</div>
</div>
<!-- ./col -->
<div class="col-4 text-center">
<input type="text" class="knob" data-readonly="true" value="30" data-width="60" data-height="60"
data-fgColor="#39CCCC">
<div class="text-white">In-Store</div>
</div>
<!-- ./col -->
</div>
<!-- /.row -->
</div>
<!-- /.card-footer -->
</div>
<!-- /.card -->
<!-- Calendar -->
<div class="card bg-gradient-success">
<div class="card-header border-0">
<h3 class="card-title">
<i class="far fa-calendar-alt"></i>
Calendar
</h3>
<!-- tools card -->
<div class="card-tools">
<!-- button with a dropdown -->
<div class="btn-group">
<button type="button" class="btn btn-success btn-sm dropdown-toggle" data-toggle="dropdown" data-offset="-52">
<i class="fas fa-bars"></i>
</button>
<div class="dropdown-menu" role="menu">
<a href="#" class="dropdown-item">Add new event</a>
<a href="#" class="dropdown-item">Clear events</a>
<div class="dropdown-divider"></div>
<a href="#" class="dropdown-item">View calendar</a>
</div>
</div>
<button type="button" class="btn btn-success btn-sm" data-card-widget="collapse">
<i class="fas fa-minus"></i>
</button>
<button type="button" class="btn btn-success btn-sm" data-card-widget="remove">
<i class="fas fa-times"></i>
</button>
</div>
<!-- /. tools -->
</div>
<!-- /.card-header -->
<div class="card-body pt-0">
<!--The calendar -->
<div id="calendar" style="width: 100%"></div>
</div>
<!-- /.card-body -->
</div>
<!-- /.card -->
</section>
<!-- right col -->
</div>

View File

@ -1,6 +1,6 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="headerFragment">
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<th:block th:fragment="headerFragment">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@ -37,6 +37,16 @@
<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}">
<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}">
<!-- SweetAlert2 CSS -->
<link rel="stylesheet" th:href="@{https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css}" >
<!-- CSS -->
@ -78,6 +88,8 @@
<script th:src="@{/plugins/toastr/toastr.min.js}"></script>
<!-- SweetAlert2 JS -->
<script th:src="@{https://cdn.jsdelivr.net/npm/sweetalert2@11}"></script>
<script>
@ -133,5 +145,6 @@
}
</script>
</head>
<!-- 개별 페이지에서 스타일/스크립트 넣을 수 있게 확장 지점 추가 -->
</th:block>
</html>

View File

@ -92,11 +92,11 @@
</a>
<ul class="nav nav-treeview">
<li class="nav-item">
<a th:href="@{/itn/bizTrip/reg}" class="nav-link">
<a th:href="@{/itn/bizTrip/list}" 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>
<p>출장 등록</p>
<p>출장</p>
</a>
</li>
</ul>

View File

@ -136,12 +136,22 @@
<i class="fas fa-expand-arrows-alt"></i>
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-widget="control-sidebar" data-controlsidebar-slide="true" href="#" role="button">
<i class="fas fa-th-large"></i>
</a>
<!-- <li class="nav-item">-->
<!-- <a class="nav-link" data-widget="control-sidebar" data-controlsidebar-slide="true" href="#" role="button">-->
<!-- <i class="fas fa-th-large"></i>-->
<!-- </a>-->
<!-- </li>-->
<li class="nav-item d-none d-sm-inline-block">
<span class="nav-link">
<i class="fas fa-user-circle text-primary mr-1 fs-3"></i>
<strong class="text-dark fs-3 fw-semibold"
th:text="${session.loginVO.userName}">
사용자</strong>
</span>
</li>
<li class="nav-item d-none d-sm-inline-block">
<a th:href="@{/logout}" class="nav-link">logout</a>
</li>

View File

@ -0,0 +1,334 @@
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="layout">
<head>
<th:block layout:fragment="title">
<title>출장 수정</title>
</th:block>
<th:block layout:fragment="head">
<style>
.table-form th {
background-color: #f1f1f1;
text-align: left;
vertical-align: middle;
padding: 10px;
width: 15%;
}
.table-form td {
padding: 10px;
}
.card-header {
background-color: #ffffff;
font-weight: bold;
border-top: 2px solid #009fe3;
}
</style>
</th:block>
</head>
<body layout:fragment="body">
<div class="wrapper">
<div th:replace="~{fragments/top_nav :: topFragment}"/>
<aside class="main-sidebar sidebar-dark-primary elevation-4"
th:insert="~{fragments/mainsidebar :: sidebarFragment}"></aside>
<div class="content-wrapper">
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0">출장 수정</h1>
</div>
</div>
</div>
</div>
<section class="content">
<div class="container-fluid">
<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;">
<tbody>
<tr>
<th>출장구분</th>
<td>
<select class="form-control" id="tripType" name="tripType">
<option value="">-- 선택 --</option>
<option th:each="code : ${@TCodeUtils.getCodeList('TRIP_TYPE')}"
th:value="${code.codeId}"
th:selected="${trip.tripTypeCd == code.codeId}"
th:text="${code.codeName}">
</option>
</select>
</td>
<th>출장지</th>
<td>
<select class="form-control d-inline" style="width: 20%;" id="tripLocation">
<option value="">-- 지역 --</option>
<option th:each="code : ${@TCodeUtils.getCodeList('TRIP_LOCATION')}"
th:value="${code.codeId}"
th:selected="${trip.locationCd == code.codeId}"
th:text="${code.codeName}">
</option>
</select>
<input type="text" id="locationTxt" class="form-control d-inline" style="width: 70%;" th:value="${trip.locationTxt}" placeholder="목적지"/>
</td>
</tr>
<tr>
<th>출장목적</th>
<td><input type="text" id="purpose" class="form-control" th:value="${trip.purpose}"/></td>
<th>이동사항</th>
<td>
<select class="form-control" id="tripMove">
<option value="">-- 선택 --</option>
<option th:each="code : ${@TCodeUtils.getCodeList('TRIP_MOVE')}"
th:value="${code.codeId}"
th:selected="${trip.moveCd == code.codeId}"
th:text="${code.codeName}">
</option>
</select>
</td>
</tr>
<tr>
<th>출장일자</th>
<td><input type="date" id="tripDate" class="form-control" th:value="${#temporals.format(trip.tripDt, 'yyyy-MM-dd')}"/></td>
<th>시간</th>
<td>
<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"
th:value="${trip.startTime}"/>
<div class="input-group-append" data-target="#startTimePicker">
<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"
th:value="${trip.endTime}"/>
<div class="input-group-append" data-target="#endTimePicker">
<div class="input-group-text"><i class="far fa-clock"></i></div>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<!-- 출장 인원 영역/결재라인은 reg.html 복사 후 바인딩만 유지 -->
<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>이름</th>
<th>부서명</th>
<th>연락처</th>
<th style="width: 15%;" class="text-center">
<button type="button" class="btn btn-info btn-sm" data-toggle="modal" data-target="#userSearchModal">
<i class="fas fa-user-plus"></i> 인원 추가
</button>
</th>
</tr>
</thead>
<tbody id="tripMemberTbody">
<tr th:each="member : ${trip.memberList}"
th:data-uniqid="${member.uniqId}">
<td th:text="${@TCodeUtils.getUserName(member.uniqId)}"/>
<td th:text="${@TCodeUtils.getUserDeptTxt(member.uniqId)}"/>
<td th:text="${@TCodeUtils.getUserMobilePhone(member.uniqId)}"/>
<td class="text-center"
th:if="${member.role == '0'}"
th:text="${member.role == '0' ? '기안자' : ''}"/>
<td class="text-center"
th:unless="${member.role == '0'}">
<a href="#" class="btn btn-danger btn-sm" onclick="removeMemberRow(this)">
<i class="fas fa-user-minus"></i> 삭제
</a>
</td>
</tr>
</tbody>
</table>
</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 th:each="line, iterStat : ${trip.approvalList}">
<td class="text-center align-middle"
th:text="${line.orderNo == 3 ? '결재' : '검토 ' + line.orderNo}">결재 단계</td>
<th:block th:if="${line.approverId != null}">
<td th:text="${@TCodeUtils.getUserName(line.approverId)}">이름</td>
<td th:text="${@TCodeUtils.getUserDeptTxt(line.approverId)}">부서</td>
<td th:text="${@TCodeUtils.getUserMobilePhone(line.approverId)}">연락처</td>
<td class="text-center">
<a href="#" class="btn btn-danger btn-sm" th:onclick="|resetApproval('approval${line.orderNo}')|">
<i class="fas fa-user-minus"></i> 삭제
</a>
</td>
<!-- 히든값 설정 -->
<input type="hidden" th:id="'approver' + ${line.orderNo}"
th:name="'approver' + ${line.orderNo}" th:value="${line.approverId}" />
</th:block>
<th:block th:if="${line.approverId == null}">
<td colspan="4">
<button type="button" class="btn btn-outline-info btn-sm"
th:onclick="|openApprovalModal('approval${line.orderNo}')|">
<i class="fas fa-user-plus"></i> 사용자 지정
</button>
</td>
</th:block>
</tr>
</tbody>
</table>
</div>
</div>
<div class="text-right mt-2">
<button type="submit" class="btn btn-primary btn-sm" th:onclick="updateTripData([[${trip.tripId}]]);">
<i class="fas fa-save"></i> 수정
</button>
</div>
</div>
</div>
</div>
</section>
</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">
<div class="modal-content">
<div class="modal-header bg-info">
<h5 class="modal-title text-white" id="userSearchModalLabel">사용자 검색</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="userSearchKeyword" 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="userSearchResult">
<!-- 검색 결과 동적 렌더링 -->
<!-- 예시 -->
</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>
<!-- /.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 -->
<script th:src="@{/cmn/js/bizTrip/edit/init.js}"></script>
<script th:src="@{/cmn/js/bizTrip/edit/event.js}"></script>
<script th:src="@{/cmn/js/bizTrip/edit/service.js}"></script>
<script th:src="@{/cmn/js/bizTrip/edit/validation.js}"></script>
</body>
</html>

View File

@ -5,15 +5,13 @@
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}">
<th:block layout:fragment="title">
<title>출장 목록</title>
</th:block>
<th:block layout:fragment="head">
<style>
.cursor-pointer {
cursor: pointer;
@ -23,6 +21,7 @@
color: #007bff;
}
</style>
</th:block>
</head>
<body layout:fragment="body">
@ -42,12 +41,12 @@
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0">사용자 관리</h1>
<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>
<li class="breadcrumb-item active">장 목록</li>
</ol>
</div><!-- /.col -->
</div><!-- /.row -->
@ -80,46 +79,54 @@
<th>목적</th>
<th>이동수단</th>
<th>상태</th>
<th>결재상태</th>
<th>작성자</th>
<!-- <th>결재상태</th>-->
<th>결재대기자</th>
<th>기안자</th>
<th style="width: 100px;">관리</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>
<tr th:each="row, stat : ${list}"
th:onclick="|location.href='@{/itn/bizTrip/detail/{tripId}(tripId=${row.tripId})}'|"
class="cursor-pointer">
<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 px-3 py-2 fs-6">대기</span>
<span th:case="'20'" class="badge badge-primary px-3 py-2 fs-6">진행</span>
<span th:case="'30'" class="badge badge-success px-3 py-2 fs-6">승인</span>
<span th:case="'40'" class="badge badge-danger px-3 py-2 fs-6">반려</span>
<span th:case="*">-</span>
</span>
</td>
<td th:if="${row.status=='10' or row.status=='20'}" th:text="${@TCodeUtils.getUserName(row.currentApproverId)}"/>
<td th:unless="${row.status=='10' or row.status=='20'}" >-</td>
<td th:text="${@TCodeUtils.getUserName(row.frstRegisterId)}"/>
<td class="text-center">
<div th:if="${row.status == '10' and (row.frstRegisterId == loginUser.uniqId or loginUser.role.name() == 'ROLE_ADMIN')}">
<a th:href="@{/itn/bizTrip/edit/{tripId}(tripId=${row.tripId})}" class="btn btn-sm btn-warning">수정</a>
<button type="button" class="btn btn-sm btn-danger" th:onclick="|deleteTrip('${row.tripId}')|">삭제</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- /.card-body -->
<div class="card-footer text-right">
<a href="/itn/bizTrip/reg" class="btn btn-primary">
<i class="fas fa-plus"></i> 출장 등록
</a>
</div>
</div>
<!-- /.card -->
</div>
@ -207,7 +214,7 @@
exportOptions: commonExportOptions
},
"colvis"]
}).buttons().container().appendTo('#commuteTb_wrapper .col-md-6:eq(0)');
}).buttons().container().appendTo('#tripTb_wrapper .col-md-6:eq(0)');

View File

@ -10,9 +10,12 @@
<!-- 필요하다면 개별 파일에 사용될 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}">
<th:block layout:fragment="title">
<title>출장 등록</title>
</th:block>
<th:block layout:fragment="head">
<style>
.table-form th {
@ -34,6 +37,7 @@
}
</style>
</th:block>
</head>
<body layout:fragment="body">
@ -68,13 +72,8 @@
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<!-- <h3>출장 등록</h3>-->
<!-- 출장신청 영역 -->
<div class="card mb-4">
<div class="card-header">
출장정보
</div>
<div class="card-header">출장정보</div>
<div class="card-body">
<table class="table table-bordered table-form" style="table-layout: fixed;">
<tbody>
@ -146,7 +145,7 @@
</div>
</div>
</div>
</td>
</td>
</tr>
</tbody>
</table>
@ -352,10 +351,10 @@
</div>
<!-- ./wrapper -->
<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>
<script th:src="@{/cmn/js/bizTrip/reg/init.js}"></script>
<script th:src="@{/cmn/js/bizTrip/reg/event.js}"></script>
<script th:src="@{/cmn/js/bizTrip/reg/service.js}"></script>
<script th:src="@{/cmn/js/bizTrip/reg/validation.js}"></script>
</body>

View File

@ -0,0 +1,260 @@
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="layout">
<head>
<th:block layout:fragment="title">
<title>출장 상세</title>
</th:block>
<th:block layout:fragment="head">
<style>
.table-form th {
background-color: #f1f1f1;
text-align: left;
vertical-align: middle;
padding: 10px;
width: 15%;
}
.table-form td {
padding: 10px;
}
.card-header {
background-color: #ffffff;
font-weight: bold;
border-top: 2px solid #009fe3;
}
.approve-date {
font-size: 0.85rem;
margin-left: 10px;
color: #6c757d;
display: inline-flex;
align-items: center;
}
.approve-date i {
margin-right: 4px;
}
</style>
</th:block>
</head>
<body layout:fragment="body">
<div class="wrapper">
<div th:replace="~{fragments/top_nav :: topFragment}"/>
<aside class="main-sidebar sidebar-dark-primary elevation-4"
th:insert="~{fragments/mainsidebar :: sidebarFragment}"></aside>
<div class="content-wrapper">
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0">출장 상세</h1>
</div>
<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>
</div>
</div>
</div>
<section class="content">
<div class="container-fluid">
<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;">
<tbody>
<tr>
<th>출장구분</th>
<td th:text="${@TCodeUtils.getCodeName('TRIP_TYPE', trip.tripTypeCd)}"></td>
<th>출장지</th>
<td>
<span th:text="${@TCodeUtils.getCodeName('TRIP_LOCATION', trip.locationCd)}"/> -
<span th:text="${trip.locationTxt}"/>
</td>
</tr>
<tr>
<th>출장목적</th>
<td th:text="${trip.purpose}"/>
<th>이동사항</th>
<td th:text="${@TCodeUtils.getCodeName('TRIP_MOVE', trip.moveCd)}"/>
</tr>
<tr>
<th>출장일자</th>
<td th:text="${#temporals.format(trip.tripDt, 'yyyy-MM-dd')}"/>
<th>시간</th>
<td th:text="${trip.startTime + ' ~ ' + trip.endTime}"/>
</tr>
</tbody>
</table>
<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>이름</th>
<th>부서명</th>
<th>연락처</th>
<th>역할</th>
</tr>
</thead>
<tbody>
<tr th:each="member : ${trip.memberList}">
<td th:text="${@TCodeUtils.getUserName(member.uniqId)}"/>
<td th:text="${@TCodeUtils.getUserDeptTxt(member.uniqId)}"/>
<td th:text="${@TCodeUtils.getUserMobilePhone(member.uniqId)}"/>
<td class="text-center"
th:text="${member.role == '0' ? '기안자' : ''}">
</tr>
</tbody>
</table>
</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>결재 단계</th>
<th>이름</th>
<th>부서명(직급)</th>
<th>연락처</th>
<th>상태</th>
</tr>
</thead>
<tbody>
<tr th:each="approval, stat : ${trip.approvalList}"
th:classappend="${approval.approverId == firstWaitingApproverId} ? 'table-warning'">
<td th:if="${stat.last}" class="text-success font-weight-bold">결제</td>
<td th:unless="${stat.last}" class="text-primary">검토</td>
<td th:text="${@TCodeUtils.getUserName(approval.approverId)}"/>
<td th:text="${@TCodeUtils.getUserDeptTxt(approval.approverId) + ' ('+@TCodeUtils.getUserRankTxt(approval.approverId)+')'}"/>
<td th:text="${@TCodeUtils.getUserMobilePhone(approval.approverId)}"/>
<td>
<!-- 대기 상태 - 본인 아님 -->
<span th:if="${approval.approveStatus == '10' and loginUser.uniqId != approval.approverId}"
class="badge badge-warning px-3 py-2 fs-6">대기</span>
<!-- 대기 상태 - 본인일 때 버튼 -->
<div th:if="${approval.approveStatus == '10' and loginUser.uniqId == approval.approverId}">
<button class="btn btn-sm btn-success"
th:attr="data-id=${approval.id}, data-approver=${approval.approverId}, data-trip-id=${trip.tripId} "
onclick="handleApprove(this, 30)">승인
</button>
<button class="btn btn-sm btn-danger"
th:attr="data-id=${approval.id}, data-approver=${approval.approverId}, data-trip-id=${trip.tripId} "
onclick="handleApprove(this, 40)">반려
</button>
</div>
<!-- 승인 상태 -->
<div th:if="${approval.approveStatus == '30'}">
<span class="badge badge-success px-3 py-2 fs-6">승인</span>
<small class="approve-date">
<i class="far fa-clock"></i>
<span th:text="${#dates.format(approval.approveDt, 'yyyy-MM-dd HH:mm')}"></span>
</small>
</div>
<!-- 반려 상태 -->
<div th:if="${approval.approveStatus == '40'}">
<span class="badge badge-danger px-3 py-2 fs-6">반려</span>
<small class="approve-date">
<i class="far fa-clock"></i>
<span th:text="${#dates.format(approval.approveDt, 'yyyy-MM-dd HH:mm')}"></span>
</small>
</div>
<!-- 예외 상태 -->
<span th:if="${approval.approveStatus != '10' and approval.approveStatus != '30' and approval.approveStatus != '40'}"
class="badge badge-secondary px-3 py-2 fs-6">-</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card-footer text-right">
<a href="/itn/bizTrip/list" class="btn btn-secondary">
<i class="fas fa-list"></i> 목록
</a>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
<script>
function handleApprove(btn, approveStatus) {
const id = btn.dataset.id;
const approverId = btn.dataset.approver;
const tripId = btn.dataset.tripId;
console.log("id : ", id, ", approverId : ", approverId);
console.log("tripId : ", tripId);
const bizTripApproval = {
id: id,
tripId: tripId,
approverId: approverId,
approveStatus: approveStatus
};
// Ajax 전송
$.ajax({
url: '/api/bizTrip/approval',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(bizTripApproval),
success: function(data) {
Swal.fire({
title: data.msg,
text: '목록으로 이동하시겠습니까?',
icon: 'success',
showCancelButton: true,
confirmButtonText: '목록',
cancelButtonText: '취소'
}).then((result) => {
if (result.isConfirmed) {
location.href = "/itn/bizTrip/list";
} else if (result.isDismissed) {
location.reload();
}
});
},
error: function(xhr, data) {
fn_failedAlert("실패", data.msg);
}
});
}
</script>
</body>
</html>

View File

@ -1,8 +1,21 @@
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<!-- layout:decorate="layout">-->
<head th:replace="~{fragments/header :: headerFragment}"/>
<body class="hold-transition sidebar-mini layout-fixed" layout:fragment="body">
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<!-- <title layout:title-pattern="$DECORATOR_TITLE - MySite">기본 타이틀</title>-->
<title layout:fragment="title">ITN ADMIN</title>
<!-- 공통 헤더 (스타일/스크립트 포함) -->
<th:block th:replace="~{fragments/header :: headerFragment}" />
<!-- 개별 페이지의 head fragment 주입 위치 -->
<layout:fragment th:fragment="head" />
</head>
<body class="hold-transition sidebar-mini layout-fixed" layout:fragment="body">
</body>
</html>

View File

@ -74,7 +74,7 @@
<div class="row">
<div class="col-8">
<div class="icheck-primary">
<input type="checkbox" id="remember">
<input type="checkbox" id="remember" name="remember-me">
<label for="remember">
Remember Me
</label>