Vue3로 조직도를 보여주는 화면을 만들어보았습니다.
조금씩 Vue에 익숙해지는 느낌이 들뻔했습니다...
아직도 라이프사이클 순서가 어렵습니다...
특히 재귀함수로 만들다 보니깐 중간에 흐름을 놓치면 멍때리고 있는 나를 만날 수 있습니다.
디자인 적용 전
디자인 적용 후
체크박스가 없다면 아주 간단한 기능이 될 수 있었지만 체크박스가 있음으로 인해 많이 복잡해집니다. 예를 들어 토글을 열지 않고 체크를 하는 경우 해당 부서 내의 모든 사용자를 체크 상태로 바꿔야합니다. 또한 하위 부서나 사용자 체크 박스를 해제하면 상위 부서의 체크 박스를 해제해줘야 하기 때문에 토글 버튼 상태와 체크 박스 상태를 계속해서 추적하며 관리해야합니다.
가장 어려웠던 점은 부서의 depth가 제한이 없다는 점입니다. 사용자 데이터와 부서 데이터를 가져와서 어떻게 tree를 만들 것이며 tree를 만들고 나서 어떻게 화면에 보여줄지 생각하는게 신입인 저에게는 굉장히 어려웠습니다.
결론은 트리를 만들고 재귀적으로 자식 노드들을 보여주도록 만들었습니다.
아래 organizationChart.js 파일은 pinia를 이용하여 한 곳에서 트리를 만드는 코드입니다. 직접 props로 넘기기에는 복잡하고 잘 이해가 안가고 체크 박스나 토글 상태도 여러 클래스에서 공유해야 한다는 점 때문에 pinia를 사용했습니다.
코드는 민감한 정보가 있을 수 있어서 그냥 핵심 함수들만 가져왔습니다. 그리고 변수 이름도 다 수정해서 이대로 실행하면 동작하지는 않습니다.
동작 순서는 아래와 같습니다.
검색 모드일 때는 사용자를 직접 입력해 검색하고 조직도 모드일 때는 조직도를 통해 사용자를 찾을 수 있습니다.
RootNode에서는 MemberNode인 경우에는 사용자를 표시하고 DepartmentNode이고 해당 노드에 childrenList가 있으면 다시 NodeList를 렌더링 합니다.
organizationChart.js (pinia)
export const useInitOrgChartStore = defineStore('initOrgChart', () => {
const selectedNodeList = ref([])
const loadedNodeList = ref([])
/**
* 사용자 노드를 체크
*/
const updateSelectedMemberNode = (node) => {
const index = selectedNodeList.value.findIndex(
(n) => n.id === node.id && n.nodeType === node.nodeType
)
// 체크된 사용자 리스트에 없으면 체크 사용자 추가
if (index === -1) {
selectedNodeList.value.push({
id: node.id,
nodeType: node.nodeType,
userName: node.userName,
companyName: node.companyName,
isChecked: false
})
// 이미 체크된 사용자면 체크 사용자 삭제
} else {
selectedNodeList.value.splice(index, 1)
}
}
/**
* 부서의 사용자 노드를 중복 검사 후 체크
*/
const updateSelectedMemberNodeByDepartment = (node, isChecked) => {
const index = selectedNodeList.value.findIndex(
(selectedNode) => selectedNode.id === node.id && selectedNode.nodeType === node.nodeType
)
if (isChecked && index === -1) {
selectedNodeList.value.push({
id: node.id,
nodeType: node.nodeType,
userName: node.userName,
companyName: node.companyName,
isChecked: true
})
}
if (!isChecked && index !== -1) {
selectedNodeList.value.splice(index, 1)
}
}
/**
* 부서 데이터를 트리로 변환하는 함수
*/
const buildTree = (departmentList, parentId) => {
return departmentList
.filter((node) => node.doParentId === parentId)
.map((node) => {
// depth 계산: 최상위 노드의 path가 null이면 depth는 0
const depth = node.path === null || node.path === '' ? 0 : node.path.split(':').length
return {
id: node.doDepartmentId,
name: node.departmentName,
nodeType: DoNodeType.DEPARTMENT.code,
childrenList: buildTree(departmentList, node.doDepartmentId),
isChecked: false,
depth: depth
}
})
}
const groupUsersByDepartment = (userList) => {
return userList.reduce((acc, user) => {
if (!acc[user.doDepartmentId]) {
acc[user.doDepartmentId] = []
}
acc[user.doDepartmentId].push(user)
return acc
}, {})
}
/**
* 부서 트리에 사용자 데이터를 추가하는 함수
*/
const addUsersToDepartments = (currentTree, groupedUsers) => {
let totalUserCount = 0
currentTree.forEach((department) => {
if (department.nodeType === DoNodeType.DEPARTMENT.name) {
department.childrenList = department.childrenList || []
// 현재 부서에 속한 사용자들을 가져옵니다.
const usersInDepartment = groupedUsers[department.id] || []
// 사용자 노드를 생성합니다.
const userNodes = usersInDepartment.map((user) => ({
id: user.doUserId,
userName: user.koName,
companyName: user.companyName,
nodeType: DoNodeType.USER.code,
isChecked: false
}))
// 부서의 자식 노드에 사용자 노드를 추가합니다.
department.childrenList.push(...userNodes)
// 현재 부서의 사용자 수를 계산합니다.
let userCount = userNodes.length
// 하위 부서에 대해 재귀적으로 사용자 데이터를 추가하고, 사용자 수를 누적합니다.
userCount += addUsersToDepartments(department.childrenList, groupedUsers)
// 현재 부서에 속한 전체 사용자 수를 부서 노드에 저장합니다.
department.userCount = userCount
// 전체 사용자 수를 누적합니다.
totalUserCount += userCount
}
})
// 전체 트리에서의 사용자 수를 반환합니다.
return totalUserCount
}
/**
* 타입이 USER 이고 중복이 없으면 Load
*/
const loadSelectedUserList = () => {
const userListToLoad = selectedNodeList.value.filter(
(node) =>
node.nodeType === DoNodeType.USER.code &&
!loadedNodeList.value.some((loadedUser) => loadedUser.id === node.id)
)
if (userListToLoad.length > 10000) {
alert('10000개를 초과할 수 없습니다.')
return
}
loadedNodeList.value.push(...userListToLoad)
selectedNodeList.value = []
}
})
pinia 코드의 순서는 아래와 같습니다.
1. buildTree 메서드로 부서 트리(사용자 X) 만들기
2. groupUsersByDepartment 메서드로 [부서 id: 사용자 id] Map 만들기
3. addUsersToDepartments 메서드에서 buildTree에서 만든 트리, groupUsersByDepartment에서 만든 Map을 조합하여 트리의 각 부서에 사용자 노드 추가하기
위 세가지 메서드를 완료하면 화면에 표시할 트리가 완성됩니다.
화면 코드는 계속 하위 노드를 따라가면서 childList가 출력되고 childList 각 노드의 childList가 다시 재귀적으로 출력되는 형태입니다.
NodeList -> RootNode -> DepartmentNode -> NodeList -> (계속 반복 ...)
NodeList -> RootNode -> MemberNode
NodeList.vue
<template>
<!-- 사용자 노드 먼저 출력 -->
<template v-for="node in props.tree" :key="'user-' + node.id">
<RootNode v-if="node.nodeType === DoNodeType.USER.code" :node="node" />
</template>
<!-- 부서 노드 나중에 출력 -->
<template v-for="node in props.tree" :key="'department-' + node.id">
<RootNode v-if="node.nodeType === DoNodeType.DEPARTMENT.code" :node="node" />
</template>
</template>
RootNode.vue
<template>
<template>
<ul :class="depthClass(node.depth)">
<li>
<MemberNode v-if="isUser" :node />
<DepartmentNode v-if="isDepartment" :node />
</li>
</ul>
</template>
</template>
DepartmentNode.vue
<template>
<div :class="depthClass(node.depth)">
<div class="group-box">
<span class="basic">
<span
:class="{ 'ico-open': isOpened, 'ico-close': !isOpened }"
@click="handleToggle"
></span>
<span class="name" @click="handleToggle">
<div class="chk-box">
<input
type="checkbox"
class="inp-chk"
:id="'ceo_chk' + node.id"
v-model="node.isChecked"
@click="updateIsDepartmentChecked"
/>
<label :for="'ceo_chk' + node.id"><em class="blind">체크</em></label>
</div>
<span class="tit">
{{ node.name }}
</span>
<strong>({{ node.userCount ? node.userCount : 0 }})</strong>
</span>
</span>
</div>
</div>
<NodeList v-if="isOpened" :tree="tree" />
</template>
MemberNode.vue
<template>
<div class="organization-division">
<div class="director-box">
<span class="basic">
<span class="name">
<div class="chk-box">
<input
class="inp-chk"
v-model="isChecked"
@change="updateIsChecked()"
type="checkbox"
:id="'managing_director_chk' + node.id"
/>
<label :for="'managing_director_chk' + node.id"><em class="blind">체크</em></label>
</div>
<span class="tit">{{ node.userName }} {{ node.position }}</span></span
>
</span>
</div>
</div>
'웹 개발 > Front End' 카테고리의 다른 글
새해 덕담 카드 만들기 (0) | 2024.12.01 |
---|---|
검색 카테고리 여러 개 동시 선택 (0) | 2024.06.15 |
vue3-excel-editor 사용법 (0) | 2024.06.01 |
Javascript를 이용한 Todo list (0) | 2022.03.16 |
부트스트랩을 이용한 사이트 만들기 (0) | 2021.11.09 |
댓글