forked from baron/baron-sso
조직현황 구조변경. 총괄센터삼안 실 조직 삽입확인
This commit is contained in:
@@ -34,7 +34,7 @@ Baron SSO 시스템과 연동되는 독립적인 **조직도 시각화(Organizat
|
||||
| 용도 | API | 주요 사용 필드 |
|
||||
| --- | --- | --- |
|
||||
| 테넌트 목록 | `GET /v1/admin/tenants?limit=10000&offset=0` | `id`, `type`, `name`, `slug`, `parentId`, `memberCount`, `status` |
|
||||
| 사용자 목록 | `GET /v1/admin/users?limit=5000&offset=0` | `id`, `email`, `name`, `status`, `tenantSlug`, `companyCode`, `joinedTenants`, `position`, `jobTitle` |
|
||||
| 사용자 목록 | `GET /v1/admin/users?limit=5000&offset=0` | `id`, `email`, `name`, `status`, `tenantSlug`, `companyCode`, `joinedTenants`, `grade`, `position`, `jobTitle` |
|
||||
|
||||
테넌트는 Baron Admin에서 입력한 `parentId` 관계를 기준으로 트리로 변환합니다. 현재 루트 후보는 `type === "COMPANY_GROUP"`인 테넌트를 우선 사용하고, 없으면 최상위 테넌트를 사용합니다. 회사 필터는 루트 하위의 `type === "COMPANY"` 테넌트로 구성됩니다.
|
||||
|
||||
@@ -47,7 +47,7 @@ Baron SSO 시스템과 연동되는 독립적인 **조직도 시각화(Organizat
|
||||
5. `joinedTenants`가 있으면 각 joined tenant의 `slug`에도 같은 사용자를 추가합니다.
|
||||
6. 같은 테넌트 노드 안에서 동일 사용자 `id`는 중복 추가하지 않습니다.
|
||||
|
||||
각 조직도 노드는 테넌트명(`name`)을 헤더로 사용하고, 해당 테넌트 `slug`에 매핑된 사용자를 구성원 목록으로 표시합니다. 구성원은 `position`/`jobTitle` 기준으로 정렬되며, 표시 직무는 `jobTitle || position || "사원"` 순서로 결정됩니다.
|
||||
각 조직도 노드는 테넌트명(`name`)을 헤더로 사용하고, 해당 테넌트 `slug`에 매핑된 사용자를 구성원 목록으로 표시합니다. 구성원은 직책(`position`) 조직장 여부와 직급(`grade`) 기준으로 정렬되며, 사용자 표시는 `이름 직급(직책)` 형식을 우선 사용하고 직책이 없으면 직무(`jobTitle`)를 보조 표시로 사용합니다.
|
||||
|
||||
### 공유 조직도 화면
|
||||
|
||||
|
||||
491
orgfront/package-lock.json
generated
491
orgfront/package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@tanstack/react-query": "^5.66.8",
|
||||
"@tanstack/react-query-devtools": "^5.66.8",
|
||||
"@xyflow/react": "^12.10.2",
|
||||
"axios": "^1.7.9",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -433,38 +434,35 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
|
||||
"integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.0",
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
|
||||
"integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
||||
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
@@ -527,9 +525,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
|
||||
"integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -584,9 +582,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.122.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
|
||||
"integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
|
||||
"version": "0.129.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz",
|
||||
"integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -1058,9 +1056,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz",
|
||||
"integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1075,9 +1073,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz",
|
||||
"integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1092,9 +1090,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz",
|
||||
"integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1109,9 +1107,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz",
|
||||
"integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1126,9 +1124,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz",
|
||||
"integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1143,13 +1141,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz",
|
||||
"integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1160,13 +1161,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz",
|
||||
"integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1177,13 +1181,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz",
|
||||
"integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1194,13 +1201,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz",
|
||||
"integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1211,13 +1221,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz",
|
||||
"integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1228,13 +1241,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz",
|
||||
"integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1245,9 +1261,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz",
|
||||
"integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1262,9 +1278,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz",
|
||||
"integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
@@ -1272,16 +1288,18 @@
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/wasm-runtime": "^1.1.1"
|
||||
"@emnapi/core": "1.10.0",
|
||||
"@emnapi/runtime": "1.10.0",
|
||||
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz",
|
||||
"integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1296,9 +1314,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz",
|
||||
"integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1380,9 +1398,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -1401,6 +1419,55 @@
|
||||
"assertion-error": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-drag": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-selection": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
||||
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-transition": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
||||
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-zoom": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
||||
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-interpolate": "*",
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/deep-eql": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||
@@ -1584,6 +1651,38 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/react": {
|
||||
"version": "12.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.2.tgz",
|
||||
"integrity": "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xyflow/system": "0.0.76",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/system": {
|
||||
"version": "0.0.76",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.76.tgz",
|
||||
"integrity": "sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-drag": "^3.0.7",
|
||||
"@types/d3-interpolate": "^3.0.4",
|
||||
"@types/d3-selection": "^3.0.10",
|
||||
"@types/d3-transition": "^3.0.8",
|
||||
"@types/d3-zoom": "^3.0.8",
|
||||
"d3-drag": "^3.0.0",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-zoom": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
@@ -1676,14 +1775,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
||||
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
|
||||
"integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
@@ -1873,6 +1972,12 @@
|
||||
"url": "https://polar.sh/cva"
|
||||
}
|
||||
},
|
||||
"node_modules/classcat": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
|
||||
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@@ -1961,6 +2066,111 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-dispatch": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-drag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-selection": "3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-selection": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-transition": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-ease": "1 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"d3-selection": "2 - 3"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-zoom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-drag": "2 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-selection": "2 - 3",
|
||||
"d3-transition": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
|
||||
@@ -2203,9 +2413,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -3051,9 +3261,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3116,9 +3326,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||
"version": "8.5.14",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3279,10 +3489,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
@@ -3463,14 +3676,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz",
|
||||
"integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.122.0",
|
||||
"@rolldown/pluginutils": "1.0.0-rc.12"
|
||||
"@oxc-project/types": "=0.129.0",
|
||||
"@rolldown/pluginutils": "1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
@@ -3479,27 +3692,27 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.0-rc.12",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.12",
|
||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.12",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.12",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.12",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.12",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.12",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12"
|
||||
"@rolldown/binding-android-arm64": "1.0.0",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.0",
|
||||
"@rolldown/binding-darwin-x64": "1.0.0",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.0",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.0",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.0",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.0",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.0",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.0",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz",
|
||||
"integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -3719,14 +3932,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
"picomatch": "^4.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
@@ -3754,9 +3967,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3929,17 +4142,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz",
|
||||
"integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==",
|
||||
"version": "8.0.12",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz",
|
||||
"integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
"postcss": "^8.5.8",
|
||||
"rolldown": "1.0.0-rc.12",
|
||||
"tinyglobby": "^0.2.15"
|
||||
"postcss": "^8.5.14",
|
||||
"rolldown": "1.0.0",
|
||||
"tinyglobby": "^0.2.16"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@@ -3955,8 +4168,8 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.1.0",
|
||||
"esbuild": "^0.27.0",
|
||||
"@vitejs/devtools": "^0.1.18",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"sass": "^1.70.0",
|
||||
@@ -4227,6 +4440,34 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@tanstack/react-query": "^5.66.8",
|
||||
"@tanstack/react-query-devtools": "^5.66.8",
|
||||
"@xyflow/react": "^12.10.2",
|
||||
"axios": "^1.7.9",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
107
orgfront/src/features/orgchart/pickerTree.test.ts
Normal file
107
orgfront/src/features/orgchart/pickerTree.test.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { TenantSummary, UserSummary } from "../../lib/adminApi";
|
||||
import { buildOrgPickerTree } from "./pickerTree";
|
||||
|
||||
function tenant(
|
||||
id: string,
|
||||
type: string,
|
||||
name: string,
|
||||
slug: string,
|
||||
parentId?: string,
|
||||
): TenantSummary {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
slug,
|
||||
description: "",
|
||||
status: "active",
|
||||
parentId,
|
||||
memberCount: 0,
|
||||
createdAt: "2026-05-11T00:00:00.000Z",
|
||||
updatedAt: "2026-05-11T00:00:00.000Z",
|
||||
};
|
||||
}
|
||||
|
||||
describe("buildOrgPickerTree", () => {
|
||||
it("uses the hanmac-family company-group as the default picker root", () => {
|
||||
const tenants = [
|
||||
tenant("wrong-group", "COMPANY_GROUP", "Wrong Group", "wrong-group"),
|
||||
tenant(
|
||||
"wrong-company",
|
||||
"COMPANY",
|
||||
"Wrong Company",
|
||||
"wrong-company",
|
||||
"wrong-group",
|
||||
),
|
||||
tenant("hanmac-family-id", "COMPANY_GROUP", "한맥가족", "hanmac-family"),
|
||||
tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"),
|
||||
];
|
||||
|
||||
const tree = buildOrgPickerTree({
|
||||
tenants,
|
||||
users: [] satisfies UserSummary[],
|
||||
});
|
||||
|
||||
expect(tree.companyGroupId).toBe("hanmac-family-id");
|
||||
expect(tree.roots).toHaveLength(1);
|
||||
expect(tree.roots[0]?.id).toBe("hanmac-family-id");
|
||||
expect(tree.roots[0]?.children.map((node) => node.id)).toEqual([
|
||||
"saman-id",
|
||||
]);
|
||||
});
|
||||
|
||||
it("scopes descendant filtering by tenant slug", () => {
|
||||
const tenants = [
|
||||
tenant("hanmac-family-id", "COMPANY_GROUP", "한맥가족", "hanmac-family"),
|
||||
tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"),
|
||||
tenant("planning-id", "ORGANIZATION", "기획팀", "planning", "saman-id"),
|
||||
tenant("hanmac-id", "COMPANY", "한맥기술", "hanmac", "hanmac-family-id"),
|
||||
];
|
||||
|
||||
const tree = buildOrgPickerTree({
|
||||
tenants,
|
||||
users: [] satisfies UserSummary[],
|
||||
tenantId: "saman",
|
||||
});
|
||||
|
||||
expect(tree.roots).toHaveLength(1);
|
||||
expect(tree.roots[0]?.id).toBe("saman-id");
|
||||
expect(tree.roots[0]?.children.map((node) => node.id)).toEqual([
|
||||
"planning-id",
|
||||
]);
|
||||
});
|
||||
|
||||
it("excludes private tenants and their descendants from picker choices", () => {
|
||||
const tenants = [
|
||||
tenant("hanmac-family-id", "COMPANY_GROUP", "한맥가족", "hanmac-family"),
|
||||
tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"),
|
||||
{
|
||||
...tenant(
|
||||
"secret-id",
|
||||
"ORGANIZATION",
|
||||
"비공개 조직",
|
||||
"secret",
|
||||
"saman-id",
|
||||
),
|
||||
config: { visibility: "private" },
|
||||
},
|
||||
tenant(
|
||||
"secret-child-id",
|
||||
"USER_GROUP",
|
||||
"비공개 하위",
|
||||
"secret-child",
|
||||
"secret-id",
|
||||
),
|
||||
tenant("open-id", "ORGANIZATION", "공개 조직", "open", "saman-id"),
|
||||
];
|
||||
|
||||
const tree = buildOrgPickerTree({
|
||||
tenants,
|
||||
users: [] satisfies UserSummary[],
|
||||
tenantId: "saman",
|
||||
});
|
||||
|
||||
expect(tree.roots[0]?.children.map((node) => node.id)).toEqual(["open-id"]);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TenantSummary, UserSummary } from "../../lib/adminApi";
|
||||
import { type TenantNode, buildTenantFullTree } from "../../lib/tenantTree";
|
||||
import type { OrgPickerTreeNode } from "./pickerTypes";
|
||||
import { filterTenantsByVisibility } from "./tenantVisibility";
|
||||
import { getOrgChartUserDisplayName } from "./userDisplay";
|
||||
|
||||
function getUserTenantSlug(user: UserSummary) {
|
||||
@@ -28,6 +29,23 @@ function getCompanyGroupId(node: TenantNode, allTenants: TenantSummary[]) {
|
||||
return cursor?.type === "COMPANY_GROUP" ? cursor.id : node.id;
|
||||
}
|
||||
|
||||
function isHanmacFamilyCompanyGroup(tenant: TenantSummary) {
|
||||
return (
|
||||
tenant.type.toUpperCase() === "COMPANY_GROUP" &&
|
||||
tenant.slug.toLowerCase() === "hanmac-family"
|
||||
);
|
||||
}
|
||||
|
||||
function findTenantByRef(tenants: TenantSummary[], ref?: string) {
|
||||
const normalizedRef = ref?.trim().toLowerCase();
|
||||
if (!normalizedRef) return undefined;
|
||||
|
||||
return (
|
||||
tenants.find((tenant) => tenant.slug.toLowerCase() === normalizedRef) ??
|
||||
tenants.find((tenant) => tenant.id === ref)
|
||||
);
|
||||
}
|
||||
|
||||
function tenantToPickerNode(
|
||||
tenant: TenantNode,
|
||||
usersBySlug: Map<string, UserSummary[]>,
|
||||
@@ -58,12 +76,34 @@ function tenantToPickerNode(
|
||||
|
||||
function findTenantNode(
|
||||
roots: TenantNode[],
|
||||
tenantId: string,
|
||||
tenantRef: string,
|
||||
): TenantNode | undefined {
|
||||
const findBySlug = (node: TenantNode): TenantNode | undefined => {
|
||||
if (node.slug.toLowerCase() === tenantRef.trim().toLowerCase()) {
|
||||
return node;
|
||||
}
|
||||
for (const child of node.children) {
|
||||
const match = findBySlug(child);
|
||||
if (match) return match;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const findById = (node: TenantNode): TenantNode | undefined => {
|
||||
if (node.id === tenantRef) return node;
|
||||
for (const child of node.children) {
|
||||
const match = findById(child);
|
||||
if (match) return match;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
for (const root of roots) {
|
||||
if (root.id === tenantId) return root;
|
||||
const child = findTenantNode(root.children, tenantId);
|
||||
if (child) return child;
|
||||
const slugMatch = findBySlug(root);
|
||||
if (slugMatch) return slugMatch;
|
||||
}
|
||||
for (const root of roots) {
|
||||
const idMatch = findById(root);
|
||||
if (idMatch) return idMatch;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -79,7 +119,10 @@ export function buildOrgPickerTree({
|
||||
rootTenantId?: string;
|
||||
tenantId?: string;
|
||||
}) {
|
||||
const visibleTenants = tenants.filter(isOrgFrontTenantType);
|
||||
const visibleTenants = filterTenantsByVisibility(
|
||||
tenants.filter(isOrgFrontTenantType),
|
||||
"internal",
|
||||
);
|
||||
const usersBySlug = new Map<string, UserSummary[]>();
|
||||
for (const user of users) {
|
||||
if (user.status !== "active") continue;
|
||||
@@ -91,7 +134,8 @@ export function buildOrgPickerTree({
|
||||
}
|
||||
|
||||
const companyGroup =
|
||||
visibleTenants.find((tenant) => tenant.id === rootTenantId) ??
|
||||
findTenantByRef(visibleTenants, rootTenantId) ??
|
||||
visibleTenants.find(isHanmacFamilyCompanyGroup) ??
|
||||
visibleTenants.find((tenant) => tenant.type === "COMPANY_GROUP") ??
|
||||
visibleTenants.find((tenant) => !tenant.parentId);
|
||||
|
||||
|
||||
34
orgfront/src/features/orgchart/pickerTypes.test.ts
Normal file
34
orgfront/src/features/orgchart/pickerTypes.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildOrgPickerEmbedSrc,
|
||||
parseOrgPickerEmbedOptions,
|
||||
} from "./pickerTypes";
|
||||
|
||||
describe("org picker embed options", () => {
|
||||
it("builds slug-based tenant scope urls", () => {
|
||||
expect(
|
||||
buildOrgPickerEmbedSrc({
|
||||
mode: "single",
|
||||
select: "tenant",
|
||||
includeDescendants: true,
|
||||
showDescendantToggle: true,
|
||||
tenantId: "saman",
|
||||
width: 400,
|
||||
height: 600,
|
||||
}),
|
||||
).toBe(
|
||||
"/embed/picker?mode=single&select=tenant&width=400&height=600&tenantSlug=saman",
|
||||
);
|
||||
});
|
||||
|
||||
it("parses tenantSlug first and keeps legacy tenantId compatibility", () => {
|
||||
expect(
|
||||
parseOrgPickerEmbedOptions(
|
||||
"?tenantId=legacy-id&tenantSlug=saman&companyTenantId=legacy-company",
|
||||
).tenantId,
|
||||
).toBe("saman");
|
||||
expect(parseOrgPickerEmbedOptions("?tenantId=legacy-id").tenantId).toBe(
|
||||
"legacy-id",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -70,7 +70,11 @@ export function parseOrgPickerEmbedOptions(search: string) {
|
||||
select: parseOrgPickerSelectableType(params.get("select")),
|
||||
includeDescendants: params.get("includeDescendants") !== "false",
|
||||
showDescendantToggle: params.get("showDescendantToggle") !== "false",
|
||||
tenantId: params.get("tenantId") ?? params.get("companyTenantId") ?? "",
|
||||
tenantId:
|
||||
params.get("tenantSlug") ??
|
||||
params.get("tenantId") ??
|
||||
params.get("companyTenantId") ??
|
||||
"",
|
||||
width: parseEmbedDimension(params.get("width"), 400),
|
||||
height: parseEmbedDimension(params.get("height"), 600),
|
||||
};
|
||||
@@ -84,9 +88,9 @@ export function buildOrgPickerEmbedSrc(options: OrgPickerEmbedOptions) {
|
||||
height: String(options.height),
|
||||
});
|
||||
|
||||
const tenantId = options.tenantId.trim();
|
||||
if (tenantId) {
|
||||
params.set("tenantId", tenantId);
|
||||
const tenantSlug = options.tenantId.trim();
|
||||
if (tenantSlug) {
|
||||
params.set("tenantSlug", tenantSlug);
|
||||
}
|
||||
|
||||
if (options.mode === "multiple") {
|
||||
|
||||
388
orgfront/src/features/orgchart/routes/OrgChartPage.test.tsx
Normal file
388
orgfront/src/features/orgchart/routes/OrgChartPage.test.tsx
Normal file
@@ -0,0 +1,388 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
type OrgNode,
|
||||
buildOrgSelectionOptions,
|
||||
clampScale,
|
||||
getOrgNodeHeaderFill,
|
||||
getSemanticZoomMode,
|
||||
layoutForest,
|
||||
} from "./OrgChartPage";
|
||||
|
||||
function orgNode(id: string, children: OrgNode[] = [], level = 0): OrgNode {
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
level,
|
||||
members: [],
|
||||
children,
|
||||
totalCount: 0,
|
||||
totalMemberIds: new Set<string>(),
|
||||
companyCode: id,
|
||||
type: level === 0 ? "COMPANY" : "USER_GROUP",
|
||||
};
|
||||
}
|
||||
|
||||
function member(id: string) {
|
||||
return {
|
||||
id,
|
||||
email: `${id}@example.com`,
|
||||
name: id,
|
||||
role: "user",
|
||||
status: "active",
|
||||
companyCode: "root",
|
||||
grade: "사원",
|
||||
createdAt: "2026-05-11T00:00:00.000Z",
|
||||
updatedAt: "2026-05-11T00:00:00.000Z",
|
||||
};
|
||||
}
|
||||
|
||||
function tenantNode(
|
||||
id: string,
|
||||
type: string,
|
||||
name: string,
|
||||
slug: string,
|
||||
children = [],
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
slug,
|
||||
children,
|
||||
description: "",
|
||||
status: "active",
|
||||
memberCount: 0,
|
||||
recursiveMemberCount: 0,
|
||||
createdAt: "2026-05-11T00:00:00.000Z",
|
||||
updatedAt: "2026-05-11T00:00:00.000Z",
|
||||
};
|
||||
}
|
||||
|
||||
function getNodeBoundsAspectRatio(
|
||||
nodes: ReturnType<typeof layoutForest>["nodes"],
|
||||
) {
|
||||
const minX = Math.min(...nodes.map((node) => node.x));
|
||||
const maxX = Math.max(...nodes.map((node) => node.x + node.width));
|
||||
const minY = Math.min(...nodes.map((node) => node.y));
|
||||
const maxY = Math.max(...nodes.map((node) => node.y + node.height));
|
||||
|
||||
return (maxX - minX) / (maxY - minY);
|
||||
}
|
||||
|
||||
describe("org chart layout", () => {
|
||||
it("keeps small sibling groups horizontal in automatic mode", () => {
|
||||
const children = Array.from({ length: 4 }, (_, index) =>
|
||||
orgNode(`child-${index + 1}`, [], 1),
|
||||
);
|
||||
const layout = layoutForest([orgNode("root", children)], new Set());
|
||||
const childNodes = layout.nodes.filter((node) =>
|
||||
node.node.id.startsWith("child-"),
|
||||
);
|
||||
|
||||
expect(new Set(childNodes.map((node) => node.y)).size).toBe(1);
|
||||
});
|
||||
|
||||
it("uses member columns in node bounds when member count exceeds five", () => {
|
||||
const compactMembers = Array.from({ length: 6 }, (_, index) =>
|
||||
member(`member-${index + 1}`),
|
||||
);
|
||||
const node = {
|
||||
...orgNode("root"),
|
||||
members: compactMembers,
|
||||
totalCount: compactMembers.length,
|
||||
totalMemberIds: new Set(compactMembers.map((item) => item.id)),
|
||||
};
|
||||
const layout = layoutForest([node], new Set());
|
||||
const rootNode = layout.nodes.find((item) => item.node.id === "root");
|
||||
|
||||
expect(rootNode).toBeDefined();
|
||||
expect(rootNode?.width).toBeGreaterThan(340);
|
||||
expect(rootNode?.height).toBeLessThan(42 + 24 + 6 * 24);
|
||||
expect(layout.width).toBeGreaterThan((rootNode?.width ?? 0) + 72 * 2 - 1);
|
||||
});
|
||||
|
||||
it("adds one member column per five-member quotient", () => {
|
||||
const tenMembers = Array.from({ length: 10 }, (_, index) =>
|
||||
member(`member-${index + 1}`),
|
||||
);
|
||||
const sixMembers = tenMembers.slice(0, 6);
|
||||
const sixLayout = layoutForest(
|
||||
[
|
||||
{
|
||||
...orgNode("six"),
|
||||
members: sixMembers,
|
||||
totalCount: sixMembers.length,
|
||||
totalMemberIds: new Set(sixMembers.map((item) => item.id)),
|
||||
},
|
||||
],
|
||||
new Set(),
|
||||
);
|
||||
const tenLayout = layoutForest(
|
||||
[
|
||||
{
|
||||
...orgNode("ten"),
|
||||
members: tenMembers,
|
||||
totalCount: tenMembers.length,
|
||||
totalMemberIds: new Set(tenMembers.map((item) => item.id)),
|
||||
},
|
||||
],
|
||||
new Set(),
|
||||
);
|
||||
const sixNode = sixLayout.nodes.find((item) => item.node.id === "six");
|
||||
const tenNode = tenLayout.nodes.find((item) => item.node.id === "ten");
|
||||
|
||||
expect(sixNode?.width).toBeGreaterThan(340);
|
||||
expect(tenNode?.width).toBeGreaterThan(sixNode?.width ?? 0);
|
||||
expect(tenNode?.height).toBeLessThan(42 + 24 + 10 * 24);
|
||||
expect(tenLayout.width).toBeGreaterThan(sixLayout.width);
|
||||
});
|
||||
|
||||
it("uses multi-column layout by default when sibling width crosses the threshold", () => {
|
||||
const children = Array.from({ length: 13 }, (_, index) =>
|
||||
orgNode(`child-${index + 1}`, [], 1),
|
||||
);
|
||||
const layout = layoutForest([orgNode("root", children)], new Set());
|
||||
const childNodes = layout.nodes.filter((node) =>
|
||||
node.node.id.startsWith("child-"),
|
||||
);
|
||||
const uniqueChildRows = new Set(childNodes.map((node) => node.y));
|
||||
const childSpan =
|
||||
Math.max(...childNodes.map((node) => node.x + node.width)) -
|
||||
Math.min(...childNodes.map((node) => node.x));
|
||||
const aspectRatio = getNodeBoundsAspectRatio(layout.nodes);
|
||||
|
||||
expect(childNodes).toHaveLength(13);
|
||||
expect(uniqueChildRows.size).toBeGreaterThan(1);
|
||||
expect(aspectRatio).toBeGreaterThanOrEqual(1.41);
|
||||
expect(aspectRatio).toBeLessThanOrEqual(1.61);
|
||||
expect(childSpan).toBeLessThan(13 * 340 + 12 * 80);
|
||||
expect(
|
||||
layout.edges.filter((edge) => edge.key.startsWith("root->")),
|
||||
).toHaveLength(13);
|
||||
expect(
|
||||
layout.edges.filter(
|
||||
(edge) => edge.key.startsWith("root->") && edge.visibleByDefault,
|
||||
),
|
||||
).toHaveLength(new Set(childNodes.map((node) => node.x)).size);
|
||||
});
|
||||
|
||||
it("tunes column and row gaps after column selection to keep auto layout near the target aspect ratio", () => {
|
||||
const children = Array.from({ length: 5 }, (_, index) =>
|
||||
orgNode(`child-${index + 1}`, [], 1),
|
||||
);
|
||||
const layout = layoutForest([orgNode("root", children)], new Set());
|
||||
const childNodes = layout.nodes.filter((node) =>
|
||||
node.node.id.startsWith("child-"),
|
||||
);
|
||||
const aspectRatio = getNodeBoundsAspectRatio(layout.nodes);
|
||||
|
||||
expect(new Set(childNodes.map((node) => node.x)).size).toBe(2);
|
||||
expect(aspectRatio).toBeGreaterThanOrEqual(1.41);
|
||||
expect(aspectRatio).toBeLessThanOrEqual(1.61);
|
||||
});
|
||||
|
||||
it("keeps direct siblings on one level in top-down mode", () => {
|
||||
const children = Array.from({ length: 13 }, (_, index) =>
|
||||
orgNode(`child-${index + 1}`, [], 1),
|
||||
);
|
||||
const layout = layoutForest([orgNode("root", children)], new Set(), {
|
||||
childLayoutMode: "topDown",
|
||||
});
|
||||
const childNodes = layout.nodes.filter((node) =>
|
||||
node.node.id.startsWith("child-"),
|
||||
);
|
||||
const uniqueChildRows = new Set(childNodes.map((node) => node.y));
|
||||
|
||||
expect(childNodes).toHaveLength(13);
|
||||
expect(uniqueChildRows.size).toBe(1);
|
||||
});
|
||||
|
||||
it("places children in three fixed columns with centered parent edges", () => {
|
||||
const children = Array.from({ length: 10 }, (_, index) =>
|
||||
orgNode(`child-${index + 1}`, [], 1),
|
||||
);
|
||||
const layout = layoutForest([orgNode("root", children)], new Set(), {
|
||||
childLayoutMode: "threeColumn",
|
||||
});
|
||||
const childNodes = layout.nodes.filter((node) =>
|
||||
node.node.id.startsWith("child-"),
|
||||
);
|
||||
const uniqueChildColumns = new Set(childNodes.map((node) => node.x));
|
||||
const uniqueChildRows = new Set(childNodes.map((node) => node.y));
|
||||
const rootEdges = layout.edges.filter((edge) =>
|
||||
edge.key.startsWith("root->"),
|
||||
);
|
||||
|
||||
expect(uniqueChildColumns.size).toBe(3);
|
||||
expect(uniqueChildRows.size).toBe(4);
|
||||
expect(rootEdges).toHaveLength(10);
|
||||
expect(rootEdges.filter((edge) => edge.visibleByDefault)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("places the deepest child subtree in the first multi-column section", () => {
|
||||
const children = [
|
||||
orgNode("shallow-1", [], 1),
|
||||
orgNode("shallow-2", [], 1),
|
||||
orgNode("shallow-3", [], 1),
|
||||
orgNode(
|
||||
"deep",
|
||||
[
|
||||
orgNode(
|
||||
"deep-branch",
|
||||
[orgNode("deep-leaf", [orgNode("deep-tail", [], 4)], 3)],
|
||||
2,
|
||||
),
|
||||
],
|
||||
1,
|
||||
),
|
||||
orgNode("shallow-4", [], 1),
|
||||
orgNode("shallow-5", [], 1),
|
||||
];
|
||||
const layout = layoutForest([orgNode("root", children)], new Set(), {
|
||||
childLayoutMode: "threeColumn",
|
||||
});
|
||||
const rootEdges = layout.edges.filter((edge) =>
|
||||
edge.key.startsWith("root->"),
|
||||
);
|
||||
|
||||
expect(rootEdges.map((edge) => edge.key)).toContain("root->deep");
|
||||
});
|
||||
|
||||
it("centers a parent over the full child span in multi-column mode", () => {
|
||||
const children = [
|
||||
orgNode(
|
||||
"deep",
|
||||
[
|
||||
orgNode(
|
||||
"deep-branch",
|
||||
[orgNode("deep-leaf", [orgNode("deep-tail", [], 4)], 3)],
|
||||
2,
|
||||
),
|
||||
],
|
||||
1,
|
||||
),
|
||||
...Array.from({ length: 9 }, (_, index) =>
|
||||
orgNode(`shallow-${index + 1}`, [], 1),
|
||||
),
|
||||
];
|
||||
const layout = layoutForest([orgNode("root", children)], new Set(), {
|
||||
childLayoutMode: "threeColumn",
|
||||
});
|
||||
const rootNode = layout.nodes.find((node) => node.node.id === "root");
|
||||
const directChildren = layout.nodes.filter((node) => node.node.level === 1);
|
||||
const childSpanCenter =
|
||||
(Math.min(...directChildren.map((node) => node.x + node.width / 2)) +
|
||||
Math.max(...directChildren.map((node) => node.x + node.width / 2))) /
|
||||
2;
|
||||
const rootCenter = rootNode ? rootNode.x + rootNode.width / 2 : 0;
|
||||
|
||||
expect(rootNode).toBeDefined();
|
||||
expect(rootCenter).toBeCloseTo(childSpanCenter, 5);
|
||||
});
|
||||
|
||||
it("centers parents above the tidy child span", () => {
|
||||
const children = [
|
||||
orgNode("left", [orgNode("left-a", [], 2), orgNode("left-b", [], 2)], 1),
|
||||
orgNode("middle", [], 1),
|
||||
orgNode(
|
||||
"right",
|
||||
[orgNode("right-a", [], 2), orgNode("right-b", [], 2)],
|
||||
1,
|
||||
),
|
||||
];
|
||||
const layout = layoutForest([orgNode("root", children)], new Set(), {
|
||||
childLayoutMode: "topDown",
|
||||
});
|
||||
const rootNode = layout.nodes.find((node) => node.node.id === "root");
|
||||
const directChildren = layout.nodes.filter((node) =>
|
||||
["left", "middle", "right"].includes(node.node.id),
|
||||
);
|
||||
const childSpanCenter =
|
||||
(Math.min(...directChildren.map((node) => node.x + node.width / 2)) +
|
||||
Math.max(...directChildren.map((node) => node.x + node.width / 2))) /
|
||||
2;
|
||||
const rootCenter = rootNode ? rootNode.x + rootNode.width / 2 : 0;
|
||||
|
||||
expect(rootNode).toBeDefined();
|
||||
expect(rootCenter).toBeCloseTo(childSpanCenter, 5);
|
||||
});
|
||||
|
||||
it("keeps compressed subtrees from overlapping on shared vertical bands", () => {
|
||||
const layout = layoutForest(
|
||||
[
|
||||
orgNode("root", [
|
||||
orgNode(
|
||||
"left",
|
||||
[orgNode("left-a", [], 2), orgNode("left-b", [], 2)],
|
||||
1,
|
||||
),
|
||||
orgNode(
|
||||
"right",
|
||||
[orgNode("right-a", [], 2), orgNode("right-b", [], 2)],
|
||||
1,
|
||||
),
|
||||
]),
|
||||
],
|
||||
new Set(),
|
||||
);
|
||||
|
||||
for (const node of layout.nodes) {
|
||||
for (const other of layout.nodes) {
|
||||
if (node.node.id >= other.node.id) continue;
|
||||
const verticalOverlap =
|
||||
node.y < other.y + other.height && other.y < node.y + node.height;
|
||||
const horizontalOverlap =
|
||||
node.x < other.x + other.width && other.x < node.x + node.width;
|
||||
|
||||
expect(
|
||||
verticalOverlap && horizontalOverlap,
|
||||
`${node.node.id} overlaps ${other.node.id}`,
|
||||
).toBe(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps zoom limits wide enough for large SVG organization charts", () => {
|
||||
expect(clampScale(0.08)).toBe(0.08);
|
||||
expect(clampScale(5)).toBe(5);
|
||||
});
|
||||
|
||||
it("switches semantic zoom modes from overview to detail", () => {
|
||||
expect(getSemanticZoomMode(0.12)).toBe("overview");
|
||||
expect(getSemanticZoomMode(0.4)).toBe("compact");
|
||||
expect(getSemanticZoomMode(0.8)).toBe("detail");
|
||||
});
|
||||
|
||||
it("uses distinct header fills by organization depth", () => {
|
||||
expect(getOrgNodeHeaderFill(0, "family")).toBe("#000000");
|
||||
expect(getOrgNodeHeaderFill(0, "saman")).toBe("#f58220");
|
||||
expect(getOrgNodeHeaderFill(0, "hanmac")).toBe("#1e489d");
|
||||
expect(getOrgNodeHeaderFill(0, "gpdtdc")).toBe("#4b746d");
|
||||
expect(getOrgNodeHeaderFill(0, "baron")).toBe("#004cbf");
|
||||
expect(getOrgNodeHeaderFill(1, "saman")).not.toBe(
|
||||
getOrgNodeHeaderFill(0, "saman"),
|
||||
);
|
||||
expect(getOrgNodeHeaderFill(2, "saman")).not.toBe(
|
||||
getOrgNodeHeaderFill(1, "saman"),
|
||||
);
|
||||
});
|
||||
|
||||
it("orders top organization choices by the hanmac family policy", () => {
|
||||
const familyRoot = tenantNode(
|
||||
"family",
|
||||
"COMPANY_GROUP",
|
||||
"한맥가족",
|
||||
"hanmac-family",
|
||||
[
|
||||
tenantNode("saman", "COMPANY", "삼안", "saman"),
|
||||
tenantNode("baron", "COMPANY_GROUP", "바론그룹", "baron-group"),
|
||||
tenantNode("hanmac", "COMPANY", "한맥기술", "hanmac"),
|
||||
tenantNode("gpdtdc", "ORGANIZATION", "총괄기획&기술개발센터", "gpdtdc"),
|
||||
],
|
||||
);
|
||||
|
||||
expect(
|
||||
buildOrgSelectionOptions(familyRoot).map((option) => option.label),
|
||||
).toEqual(["총괄기획&기술개발센터", "삼안", "한맥기술", "바론그룹"]);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import { GitBranch, Network, PanelTop } from "lucide-react";
|
||||
import { NavLink, Outlet } from "react-router-dom";
|
||||
import { NavLink, Outlet, useLocation } from "react-router-dom";
|
||||
|
||||
const navItems = [
|
||||
{ to: "/chart", label: "조직도", icon: Network },
|
||||
@@ -8,9 +8,22 @@ const navItems = [
|
||||
];
|
||||
|
||||
export function OrgFrontLayout() {
|
||||
const location = useLocation();
|
||||
const isChartRoute =
|
||||
location.pathname === "/chart" || location.pathname.startsWith("/chart/");
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<header className="sticky top-0 z-30 border-b border-border bg-background/95 backdrop-blur">
|
||||
<div
|
||||
className={
|
||||
isChartRoute
|
||||
? "flex h-screen flex-col overflow-hidden bg-background text-foreground"
|
||||
: "min-h-screen bg-background text-foreground"
|
||||
}
|
||||
>
|
||||
<header
|
||||
className="sticky top-0 z-30 shrink-0 border-b border-border bg-background/95 backdrop-blur"
|
||||
data-testid="orgfront-topbar"
|
||||
>
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-3 px-4 py-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
||||
@@ -40,7 +53,14 @@ export function OrgFrontLayout() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="mx-auto max-w-7xl px-4 py-5">
|
||||
<main
|
||||
className={
|
||||
isChartRoute
|
||||
? "min-h-0 flex-1 overflow-hidden"
|
||||
: "mx-auto max-w-7xl px-4 py-5"
|
||||
}
|
||||
data-testid="orgfront-main"
|
||||
>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -59,7 +59,7 @@ function PickerScenarioControls({
|
||||
</label>
|
||||
|
||||
<label className="space-y-1 text-sm font-medium">
|
||||
<span className="block text-muted-foreground">tenant ID</span>
|
||||
<span className="block text-muted-foreground">tenant slug</span>
|
||||
<input
|
||||
className="h-10 w-full rounded-md border border-input bg-background px-3"
|
||||
onChange={(event) =>
|
||||
@@ -68,7 +68,7 @@ function PickerScenarioControls({
|
||||
tenantId: event.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="company-baron"
|
||||
placeholder="saman"
|
||||
type="text"
|
||||
value={options.tenantId}
|
||||
/>
|
||||
|
||||
@@ -334,6 +334,7 @@ export function OrgPickerEmbedPage() {
|
||||
const select = parseOrgPickerSelectableType(searchParams.get("select"));
|
||||
const rootTenantId = searchParams.get("rootTenantId") || undefined;
|
||||
const tenantId =
|
||||
searchParams.get("tenantSlug") ||
|
||||
searchParams.get("tenantId") ||
|
||||
searchParams.get("companyTenantId") ||
|
||||
undefined;
|
||||
@@ -615,7 +616,7 @@ export function OrgPickerPage() {
|
||||
</label>
|
||||
|
||||
<label className="space-y-1 text-sm font-medium">
|
||||
<span className="block text-muted-foreground">tenant ID</span>
|
||||
<span className="block text-muted-foreground">tenant slug</span>
|
||||
<input
|
||||
className="h-10 w-full rounded-md border border-input bg-background px-3"
|
||||
onChange={(event) =>
|
||||
@@ -624,7 +625,7 @@ export function OrgPickerPage() {
|
||||
tenantId: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="company-baron"
|
||||
placeholder="saman"
|
||||
type="text"
|
||||
value={options.tenantId}
|
||||
/>
|
||||
|
||||
45
orgfront/src/features/orgchart/tenantVisibility.ts
Normal file
45
orgfront/src/features/orgchart/tenantVisibility.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { TenantSummary } from "../../lib/adminApi";
|
||||
|
||||
export function getTenantVisibility(tenant: Pick<TenantSummary, "config">) {
|
||||
const raw = String(tenant.config?.visibility ?? "public").toLowerCase();
|
||||
if (raw === "internal" || raw === "private") return raw;
|
||||
return "public";
|
||||
}
|
||||
|
||||
export function filterTenantsByVisibility(
|
||||
tenants: TenantSummary[],
|
||||
mode: "internal" | "public",
|
||||
) {
|
||||
const excludedIds = new Set<string>();
|
||||
for (const tenant of tenants) {
|
||||
const visibility = getTenantVisibility(tenant);
|
||||
if (
|
||||
visibility === "private" ||
|
||||
(mode === "public" && visibility === "internal")
|
||||
) {
|
||||
excludedIds.add(tenant.id);
|
||||
}
|
||||
}
|
||||
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const tenant of tenants) {
|
||||
if (
|
||||
tenant.parentId &&
|
||||
excludedIds.has(tenant.parentId) &&
|
||||
!excludedIds.has(tenant.id)
|
||||
) {
|
||||
excludedIds.add(tenant.id);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tenants.filter((tenant) => !excludedIds.has(tenant.id));
|
||||
}
|
||||
|
||||
export function getOrgUnitType(config: Record<string, unknown> | undefined) {
|
||||
const value = config?.orgUnitType;
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
49
orgfront/src/features/orgchart/userDisplay.test.ts
Normal file
49
orgfront/src/features/orgchart/userDisplay.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { UserSummary } from "../../lib/adminApi";
|
||||
import { getOrgChartUserDisplayName } from "./userDisplay";
|
||||
|
||||
function user(overrides: Partial<UserSummary>): UserSummary {
|
||||
return {
|
||||
id: "user-1",
|
||||
email: "user@example.com",
|
||||
name: "홍길동",
|
||||
role: "user",
|
||||
status: "active",
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("getOrgChartUserDisplayName", () => {
|
||||
it("renders name with grade and optional position", () => {
|
||||
expect(
|
||||
getOrgChartUserDisplayName(
|
||||
user({
|
||||
grade: "수석",
|
||||
position: "팀장",
|
||||
}),
|
||||
),
|
||||
).toBe("홍길동 수석(팀장)");
|
||||
});
|
||||
|
||||
it("uses tenant appointment grade before the user grade", () => {
|
||||
expect(
|
||||
getOrgChartUserDisplayName(
|
||||
user({
|
||||
grade: "책임",
|
||||
metadata: {
|
||||
additionalAppointments: [
|
||||
{
|
||||
tenantSlug: "hanmac",
|
||||
grade: "수석",
|
||||
position: "센터장",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
{ id: "tenant-1", slug: "hanmac" },
|
||||
),
|
||||
).toBe("홍길동 수석(센터장)");
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import type { TenantSummary, UserSummary } from "../../lib/adminApi";
|
||||
type UserAppointment = {
|
||||
tenantId?: string;
|
||||
tenantSlug?: string;
|
||||
grade?: string;
|
||||
jobTitle?: string;
|
||||
position?: string;
|
||||
};
|
||||
@@ -25,6 +26,7 @@ function getUserAppointments(user: UserSummary): UserAppointment[] {
|
||||
.map((item) => ({
|
||||
tenantId: normalizeText(item.tenantId),
|
||||
tenantSlug: normalizeText(item.tenantSlug),
|
||||
grade: normalizeText(item.grade),
|
||||
jobTitle: normalizeText(item.jobTitle),
|
||||
position: normalizeText(item.position),
|
||||
}));
|
||||
@@ -44,6 +46,7 @@ export function getUserOrgProfile(user: UserSummary, tenant?: TenantIdentity) {
|
||||
});
|
||||
|
||||
return {
|
||||
grade: appointment?.grade || normalizeText(user.grade),
|
||||
jobTitle: appointment?.jobTitle || normalizeText(user.jobTitle),
|
||||
position: appointment?.position || normalizeText(user.position),
|
||||
};
|
||||
@@ -53,11 +56,12 @@ export function getOrgChartUserDisplayName(
|
||||
user: UserSummary,
|
||||
tenant?: TenantIdentity,
|
||||
) {
|
||||
const { jobTitle, position } = getUserOrgProfile(user, tenant);
|
||||
const { grade, jobTitle, position } = getUserOrgProfile(user, tenant);
|
||||
const baseName = user.name.trim();
|
||||
const detail = position || jobTitle;
|
||||
|
||||
if (jobTitle && position) return `${baseName}(${jobTitle}) ${position}`;
|
||||
if (jobTitle) return `${baseName}(${jobTitle})`;
|
||||
if (position) return `${baseName} ${position}`;
|
||||
if (grade && detail) return `${baseName} ${grade}(${detail})`;
|
||||
if (grade) return `${baseName} ${grade}`;
|
||||
if (detail) return `${baseName}(${detail})`;
|
||||
return baseName;
|
||||
}
|
||||
|
||||
@@ -388,6 +388,7 @@ export type UserSummary = {
|
||||
joinedTenants?: TenantSummary[]; // [New] 다중 소속 테넌트 목록
|
||||
metadata?: Record<string, unknown>;
|
||||
department?: string;
|
||||
grade?: string;
|
||||
position?: string;
|
||||
jobTitle?: string;
|
||||
createdAt: string;
|
||||
@@ -410,6 +411,7 @@ export type UserCreateRequest = {
|
||||
role?: string;
|
||||
tenantSlug?: string;
|
||||
department?: string;
|
||||
grade?: string;
|
||||
position?: string;
|
||||
jobTitle?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
@@ -428,6 +430,7 @@ export type UserUpdateRequest = {
|
||||
status?: string;
|
||||
tenantSlug?: string;
|
||||
department?: string;
|
||||
grade?: string;
|
||||
position?: string;
|
||||
jobTitle?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
@@ -441,6 +444,7 @@ export type BulkUserItem = {
|
||||
role?: string;
|
||||
tenantSlug?: string;
|
||||
department?: string;
|
||||
grade?: string;
|
||||
position?: string;
|
||||
jobTitle?: string;
|
||||
metadata: Record<string, string>;
|
||||
|
||||
@@ -1091,14 +1091,16 @@ email = "이메일"
|
||||
email_placeholder = "user@example.com"
|
||||
job_title = "직무"
|
||||
job_title_placeholder = "프론트엔드 개발"
|
||||
grade = "직급"
|
||||
grade_placeholder = "수석/책임/선임"
|
||||
name = "이름"
|
||||
name_placeholder = "홍길동"
|
||||
password = "비밀번호"
|
||||
password_placeholder = "********"
|
||||
phone = "전화번호"
|
||||
phone_placeholder = "010-1234-5678"
|
||||
position = "직급"
|
||||
position_placeholder = "수석/책임/선임"
|
||||
position = "직책"
|
||||
position_placeholder = "팀장/센터장"
|
||||
role = "역할"
|
||||
tenant = "테넌트"
|
||||
tenant_global = "시스템 전역"
|
||||
|
||||
@@ -8,6 +8,7 @@ type TenantFixture = {
|
||||
description: string;
|
||||
status: string;
|
||||
parentId?: string;
|
||||
config?: Record<string, unknown>;
|
||||
memberCount: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -41,7 +42,7 @@ function user(id: string, name: string, companyCode: string) {
|
||||
role: "user",
|
||||
status: "active",
|
||||
companyCode,
|
||||
position: "사원",
|
||||
grade: "사원",
|
||||
createdAt: "2026-04-01T00:00:00.000Z",
|
||||
updatedAt: "2026-04-01T00:00:00.000Z",
|
||||
};
|
||||
@@ -125,3 +126,554 @@ test("org chart viewport pans with drag and zooms with the mouse wheel", async (
|
||||
);
|
||||
expect(scale).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
test("org chart dashboard uses the full screen below the orgfront topbar", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
sharedWith: "Playwright",
|
||||
tenants: [
|
||||
tenant("root", "Baron Group", "baron"),
|
||||
tenant("engineering", "Engineering", "engineering", "root"),
|
||||
],
|
||||
users: [
|
||||
user("u-root", "Root User", "baron"),
|
||||
user("u-eng", "Engineering User", "engineering"),
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/chart?token=full-screen");
|
||||
await expect(page.getByRole("heading", { name: "조직 현황" })).toBeVisible();
|
||||
|
||||
const metrics = await page.evaluate(() => {
|
||||
const topbar = document
|
||||
.querySelector('[data-testid="orgfront-topbar"]')
|
||||
?.getBoundingClientRect();
|
||||
const main = document
|
||||
.querySelector('[data-testid="orgfront-main"]')
|
||||
?.getBoundingClientRect();
|
||||
const shell = document
|
||||
.querySelector('[data-testid="orgchart-dashboard-shell"]')
|
||||
?.getBoundingClientRect();
|
||||
|
||||
if (!topbar || !main || !shell) {
|
||||
throw new Error("Missing org chart layout elements");
|
||||
}
|
||||
|
||||
return {
|
||||
innerHeight: window.innerHeight,
|
||||
innerWidth: window.innerWidth,
|
||||
mainTop: main.top,
|
||||
shellBottom: shell.bottom,
|
||||
shellLeft: shell.left,
|
||||
shellRight: shell.right,
|
||||
shellTop: shell.top,
|
||||
topbarBottom: topbar.bottom,
|
||||
};
|
||||
});
|
||||
|
||||
expect(Math.abs(metrics.mainTop - metrics.topbarBottom)).toBeLessThanOrEqual(
|
||||
1,
|
||||
);
|
||||
expect(metrics.shellTop).toBe(metrics.topbarBottom);
|
||||
expect(metrics.shellLeft).toBeLessThanOrEqual(1);
|
||||
expect(metrics.shellRight).toBeGreaterThanOrEqual(metrics.innerWidth - 1);
|
||||
expect(metrics.shellBottom).toBeGreaterThanOrEqual(metrics.innerHeight - 1);
|
||||
});
|
||||
|
||||
test("org chart non-shared title does not render the MH Dashboard eyebrow", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("playwright_auth_bypass", "1");
|
||||
window.localStorage.setItem("dev_tenant_id", "group");
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/admin/tenants**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
...tenant("group", "Baron Group", "baron"),
|
||||
type: "COMPANY_GROUP",
|
||||
},
|
||||
tenant("engineering", "Engineering", "engineering", "group"),
|
||||
],
|
||||
limit: 10000,
|
||||
offset: 0,
|
||||
total: 2,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/admin/users**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
items: [user("u-eng", "Engineering User", "engineering")],
|
||||
limit: 5000,
|
||||
offset: 0,
|
||||
total: 1,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/chart");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "조직 현황" })).toBeVisible();
|
||||
await expect(page.getByText("MH Dashboard", { exact: true })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("org chart renders dense member nodes with calculated member columns", async ({
|
||||
page,
|
||||
}) => {
|
||||
const denseUsers = Array.from({ length: 6 }, (_, index) =>
|
||||
user(`u-dense-${index + 1}`, `Dense User ${index + 1}`, "baron"),
|
||||
);
|
||||
|
||||
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
sharedWith: "Playwright",
|
||||
tenants: [tenant("root", "Baron Group", "baron")],
|
||||
users: denseUsers,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/chart?token=dense-members");
|
||||
await expect(page.getByRole("heading", { name: "조직 현황" })).toBeVisible();
|
||||
|
||||
const rootNode = page.locator('[data-testid="orgchart-node-root"]');
|
||||
await expect(rootNode).toHaveAttribute("width", /[4-9]\d{2,}/);
|
||||
await expect(rootNode.locator('[data-member-columns="2"]')).toBeVisible();
|
||||
await expect(rootNode.getByText("Dense User 6")).toBeVisible();
|
||||
});
|
||||
|
||||
test("public org chart hides internal and private tenants and renders org unit type", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
sharedWith: "Playwright",
|
||||
tenants: [
|
||||
{
|
||||
...tenant("group", "한맥가족", "hanmac-family"),
|
||||
type: "COMPANY_GROUP",
|
||||
},
|
||||
tenant("company", "삼안", "saman", "group"),
|
||||
{
|
||||
...tenant("open-team", "공개 팀", "open-team", "company"),
|
||||
config: { orgUnitType: "팀", visibility: "public" },
|
||||
},
|
||||
{
|
||||
...tenant("internal-team", "내부 팀", "internal-team", "company"),
|
||||
config: { visibility: "internal" },
|
||||
},
|
||||
{
|
||||
...tenant("private-team", "비공개 팀", "private-team", "company"),
|
||||
config: { visibility: "private" },
|
||||
},
|
||||
tenant(
|
||||
"private-child",
|
||||
"비공개 하위",
|
||||
"private-child",
|
||||
"private-team",
|
||||
),
|
||||
],
|
||||
users: [
|
||||
user("u-open", "Open User", "open-team"),
|
||||
user("u-internal", "Internal User", "internal-team"),
|
||||
user("u-private", "Private User", "private-team"),
|
||||
user("u-private-child", "Private Child User", "private-child"),
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/chart?token=tenant-visibility");
|
||||
await expect(page.getByRole("heading", { name: "조직 현황" })).toBeVisible();
|
||||
|
||||
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
||||
await expect(svg.getByText("공개 팀", { exact: true })).toBeVisible();
|
||||
await expect(svg.getByText("팀", { exact: true })).toBeVisible();
|
||||
await expect(svg.getByText(/Open User/)).toBeVisible();
|
||||
await expect(svg.getByText("내부 팀", { exact: true })).toHaveCount(0);
|
||||
await expect(svg.getByText("Internal User", { exact: true })).toHaveCount(0);
|
||||
await expect(svg.getByText("비공개 팀", { exact: true })).toHaveCount(0);
|
||||
await expect(svg.getByText("Private User", { exact: true })).toHaveCount(0);
|
||||
await expect(svg.getByText("비공개 하위", { exact: true })).toHaveCount(0);
|
||||
await expect(
|
||||
svg.getByText("Private Child User", { exact: true }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("org chart colors hanmac family and nested baron company group separately", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
sharedWith: "Playwright",
|
||||
tenants: [
|
||||
{
|
||||
...tenant("family", "한맥가족", "hanmac-family"),
|
||||
type: "COMPANY_GROUP",
|
||||
},
|
||||
{
|
||||
...tenant("baron-group", "Baron Group", "baron-group", "family"),
|
||||
type: "COMPANY_GROUP",
|
||||
},
|
||||
{
|
||||
...tenant("baron-company", "Baron Company", "baron", "baron-group"),
|
||||
type: "COMPANY",
|
||||
},
|
||||
],
|
||||
users: [user("u-baron", "Baron User", "baron")],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/chart?token=baron-group-color");
|
||||
await expect(page.getByRole("heading", { name: "조직 현황" })).toBeVisible();
|
||||
|
||||
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
||||
await expect(svg.getByText("Baron Group", { exact: true })).toBeVisible();
|
||||
|
||||
const colors = await page.evaluate(() => {
|
||||
function headerColor(nodeId: string) {
|
||||
const node = document.querySelector(
|
||||
`[data-testid="orgchart-node-${nodeId}"]`,
|
||||
);
|
||||
const header = node?.querySelector("div > div");
|
||||
return header ? window.getComputedStyle(header).backgroundColor : "";
|
||||
}
|
||||
|
||||
return {
|
||||
baronCompany: headerColor("baron-company"),
|
||||
baronGroup: headerColor("baron-group"),
|
||||
family: headerColor("family"),
|
||||
};
|
||||
});
|
||||
|
||||
expect(colors.family).toBe("rgb(0, 0, 0)");
|
||||
expect(colors.baronGroup).toBe("rgb(0, 76, 191)");
|
||||
expect(colors.baronCompany).toBe("rgb(0, 76, 191)");
|
||||
});
|
||||
|
||||
test("org chart orders top organization choices by the hanmac family policy", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
sharedWith: "Playwright",
|
||||
tenants: [
|
||||
{
|
||||
...tenant("family", "한맥가족", "hanmac-family"),
|
||||
type: "COMPANY_GROUP",
|
||||
},
|
||||
{
|
||||
...tenant("saman", "삼안", "saman", "family"),
|
||||
type: "COMPANY",
|
||||
},
|
||||
{
|
||||
...tenant("baron-group", "바론그룹", "baron-group", "family"),
|
||||
type: "COMPANY_GROUP",
|
||||
},
|
||||
{
|
||||
...tenant("hanmac", "한맥기술", "hanmac", "family"),
|
||||
type: "COMPANY",
|
||||
},
|
||||
{
|
||||
...tenant("gpdtdc", "총괄기획&기술개발센터", "gpdtdc", "family"),
|
||||
type: "ORGANIZATION",
|
||||
},
|
||||
],
|
||||
users: [],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/chart?token=org-selection-order");
|
||||
await expect(page.getByRole("heading", { name: "조직 현황" })).toBeVisible();
|
||||
|
||||
const labels = await page
|
||||
.getByTestId("orgchart-org-selector")
|
||||
.locator("button")
|
||||
.evaluateAll((buttons) =>
|
||||
buttons.map((button) => button.textContent?.trim() ?? ""),
|
||||
);
|
||||
|
||||
expect(labels.slice(0, 5)).toEqual([
|
||||
"한맥가족",
|
||||
"총괄기획&기술개발센터",
|
||||
"삼안",
|
||||
"한맥기술",
|
||||
"바론그룹",
|
||||
]);
|
||||
});
|
||||
|
||||
test("org chart compresses many sibling organizations and allows wide zoom out", async ({
|
||||
page,
|
||||
}) => {
|
||||
const childTenants = Array.from({ length: 13 }, (_, index) =>
|
||||
tenant(
|
||||
`team-${index + 1}`,
|
||||
`Team ${index + 1}`,
|
||||
`team-${index + 1}`,
|
||||
"root",
|
||||
),
|
||||
);
|
||||
|
||||
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
sharedWith: "Playwright",
|
||||
tenants: [tenant("root", "Baron Group", "baron"), ...childTenants],
|
||||
users: childTenants.map((child, index) =>
|
||||
user(`u-team-${index + 1}`, `Team ${index + 1} User`, child.slug),
|
||||
),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/chart?token=wide-siblings");
|
||||
await expect(page.getByRole("heading", { name: "조직 현황" })).toBeVisible();
|
||||
|
||||
const viewport = page.locator('[data-testid="orgchart-viewport"]');
|
||||
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
||||
await expect(svg).toBeVisible();
|
||||
await expect(svg.getByText("Team 13", { exact: true })).toBeVisible();
|
||||
await expect(svg.locator('foreignObject[data-node-id^="team-"]')).toHaveCount(
|
||||
13,
|
||||
);
|
||||
await expect(
|
||||
page.getByRole("button", { name: "조직: 한맥가족" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("배치", { exact: true })).toBeHidden();
|
||||
await expect(page.getByRole("button", { name: "배치: 자동" })).toBeVisible();
|
||||
await expect(page.getByText("연결", { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByText("상위연결", { exact: true })).toHaveCount(0);
|
||||
|
||||
const autoChildYPositions = await svg
|
||||
.locator('foreignObject[data-node-id^="team-"]')
|
||||
.evaluateAll((nodes) =>
|
||||
nodes
|
||||
.map((node) => node.getAttribute("y") ?? "")
|
||||
.filter((value) => value.length > 0),
|
||||
);
|
||||
|
||||
expect(new Set(autoChildYPositions).size).toBeGreaterThan(1);
|
||||
await expect(svg.locator("path")).toHaveCount(13);
|
||||
await expect(
|
||||
svg.locator('path:not([data-hidden-default="true"])'),
|
||||
).toHaveCount(4);
|
||||
await expect(svg.locator('path[data-hidden-default="true"]')).toHaveCount(9);
|
||||
|
||||
await svg.locator('foreignObject[data-node-id="team-13"]').hover();
|
||||
await expect(svg.locator('path[data-highlighted="true"]')).toHaveCount(1);
|
||||
await expect(svg.locator('path[data-muted="true"]')).toHaveCount(4);
|
||||
|
||||
await page.getByTestId("orgchart-layout-mode-option").hover();
|
||||
await expect(page.getByText("배치", { exact: true })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { exact: true, name: "자동" }),
|
||||
).toHaveCount(0);
|
||||
await page.getByRole("button", { name: "Top-down" }).click();
|
||||
await expect
|
||||
.poll(async () =>
|
||||
svg
|
||||
.locator('foreignObject[data-node-id^="team-"]')
|
||||
.evaluateAll(
|
||||
(nodes) =>
|
||||
new Set(
|
||||
nodes
|
||||
.map((node) => node.getAttribute("y") ?? "")
|
||||
.filter((value) => value.length > 0),
|
||||
).size,
|
||||
),
|
||||
)
|
||||
.toBe(1);
|
||||
|
||||
await page.getByTestId("orgchart-layout-mode-option").hover();
|
||||
await page.getByRole("button", { name: "3열" }).click();
|
||||
const threeColumnPositions = await svg
|
||||
.locator('foreignObject[data-node-id^="team-"]')
|
||||
.evaluateAll((nodes) =>
|
||||
nodes.map((node) => ({
|
||||
x: node.getAttribute("x") ?? "",
|
||||
y: node.getAttribute("y") ?? "",
|
||||
})),
|
||||
);
|
||||
|
||||
expect(new Set(threeColumnPositions.map((position) => position.x)).size).toBe(
|
||||
3,
|
||||
);
|
||||
expect(new Set(threeColumnPositions.map((position) => position.y)).size).toBe(
|
||||
5,
|
||||
);
|
||||
await expect(svg.locator("path")).toHaveCount(13);
|
||||
await expect(
|
||||
svg.locator('path:not([data-hidden-default="true"])'),
|
||||
).toHaveCount(3);
|
||||
|
||||
const box = await viewport.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
if (!box) return;
|
||||
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
await page.mouse.wheel(0, 2500);
|
||||
|
||||
await expect
|
||||
.poll(async () =>
|
||||
svg.evaluate((element) =>
|
||||
Number.parseFloat(element.getAttribute("data-scale") ?? "1"),
|
||||
),
|
||||
)
|
||||
.toBeLessThan(0.45);
|
||||
});
|
||||
|
||||
test("org chart selects first and second depth organizations from company hover choices", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
sharedWith: "Playwright",
|
||||
tenants: [
|
||||
{
|
||||
...tenant("group", "Baron Group", "baron"),
|
||||
type: "COMPANY_GROUP",
|
||||
},
|
||||
{
|
||||
...tenant("company", "Company A", "company-a", "group"),
|
||||
type: "COMPANY",
|
||||
},
|
||||
tenant("department", "Department A", "department-a", "company"),
|
||||
tenant("squad", "Squad A", "squad-a", "department"),
|
||||
tenant("team", "Team A", "team-a", "squad"),
|
||||
],
|
||||
users: [
|
||||
user("u-company", "Company User", "company-a"),
|
||||
user("u-department", "Department User", "department-a"),
|
||||
user("u-squad", "Squad User", "squad-a"),
|
||||
user("u-team", "Team User", "team-a"),
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/chart?token=company-depth-filter");
|
||||
await expect(page.getByRole("heading", { name: "조직 현황" })).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole("button", { name: "조직: 한맥가족" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Company A" })).toBeVisible();
|
||||
await expect(page.getByText("하위범위", { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByText("조직", { exact: true })).toHaveCount(0);
|
||||
await page.getByRole("button", { name: "Company A" }).click();
|
||||
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
||||
await expect(svg.getByText("Team A", { exact: true })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "조직: Company A" }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page
|
||||
.getByTestId("orgchart-company-option-company")
|
||||
.getByRole("button", { name: "Company A" }),
|
||||
).toBeVisible();
|
||||
|
||||
const orgButtonColor = await page
|
||||
.getByRole("button", { name: "조직: Company A" })
|
||||
.evaluate((element) => window.getComputedStyle(element).backgroundColor);
|
||||
const layoutButtonColor = await page
|
||||
.getByRole("button", { name: "배치: 자동" })
|
||||
.evaluate((element) => window.getComputedStyle(element).backgroundColor);
|
||||
expect(orgButtonColor).not.toBe(layoutButtonColor);
|
||||
|
||||
await page.getByTestId("orgchart-company-option-company").hover();
|
||||
await expect(svg.getByText("Department A", { exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: "1뎁스 Department A" }).click();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "조직: Department A" }),
|
||||
).toBeVisible();
|
||||
await expect(svg.getByText("Squad A", { exact: true })).toBeVisible();
|
||||
|
||||
await page.getByTestId("orgchart-company-option-company").hover();
|
||||
await page.getByRole("button", { name: "2뎁스 Squad A" }).click();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "조직: Squad A" }),
|
||||
).toBeVisible();
|
||||
await expect(svg.getByText("Squad A", { exact: true })).toBeVisible();
|
||||
await expect(svg.getByText("Team A", { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("org chart uses semantic zoom to simplify deep nodes and restore labels on zoom in", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
sharedWith: "Playwright",
|
||||
tenants: [
|
||||
tenant("root", "Baron Group", "baron"),
|
||||
tenant("department", "Archive Department", "department", "root"),
|
||||
tenant("division", "Archive Division", "division", "department"),
|
||||
tenant("deep", "Archive Deep Team", "deep", "division"),
|
||||
],
|
||||
users: [
|
||||
user("u-root", "Root User", "baron"),
|
||||
user("u-department", "Department User", "department"),
|
||||
user("u-division", "Division User", "division"),
|
||||
user("u-deep", "Deep User", "deep"),
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/chart?token=semantic-zoom");
|
||||
await expect(page.getByRole("heading", { name: "조직 현황" })).toBeVisible();
|
||||
|
||||
const viewport = page.locator('[data-testid="orgchart-viewport"]');
|
||||
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
||||
const deepNode = svg.locator('foreignObject[data-node-id="deep"]');
|
||||
|
||||
await expect(svg).toHaveAttribute("data-semantic-zoom", "detail");
|
||||
await expect(deepNode.getByText("Archive Deep Team")).toBeVisible();
|
||||
|
||||
const box = await viewport.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
if (!box) return;
|
||||
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
await page.mouse.wheel(0, 4000);
|
||||
|
||||
await expect
|
||||
.poll(async () => svg.getAttribute("data-semantic-zoom"))
|
||||
.toBe("overview");
|
||||
await expect(deepNode.getByText("Archive Deep Team")).toHaveCount(0);
|
||||
|
||||
await page.mouse.wheel(0, -4000);
|
||||
|
||||
await expect
|
||||
.poll(async () => svg.getAttribute("data-semantic-zoom"))
|
||||
.toBe("detail");
|
||||
await expect(deepNode.getByText("Archive Deep Team")).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ function user(
|
||||
status: "active",
|
||||
tenantSlug,
|
||||
companyCode: tenantSlug,
|
||||
position: "사원",
|
||||
grade: "사원",
|
||||
createdAt: "2026-04-01T00:00:00.000Z",
|
||||
updatedAt: "2026-04-01T00:00:00.000Z",
|
||||
...overrides,
|
||||
@@ -173,7 +173,8 @@ async function installOrgPickerApiMock(
|
||||
user("user-platform", "Platform User", "platform", {
|
||||
metadata: { employeeNumber: "EMP-9001", skill: "Kubernetes" },
|
||||
jobTitle: "Platform Engineer",
|
||||
position: "책임",
|
||||
grade: "책임",
|
||||
position: "팀장",
|
||||
}),
|
||||
user("user-sales", "Sales User", "sales"),
|
||||
];
|
||||
@@ -252,14 +253,64 @@ test("picker menu lets developers switch selection mode and selectable type", as
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("picker displays user names with job title and position", async ({
|
||||
test("picker defaults to the hanmac-family company-group when no tenant id is supplied", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.unroute("**/api/v1/admin/tenants**");
|
||||
await page.unroute("**/api/v1/admin/users**");
|
||||
|
||||
const tenants = [
|
||||
tenant("wrong-group", "COMPANY_GROUP", "Wrong Group", "wrong-group"),
|
||||
tenant(
|
||||
"wrong-company",
|
||||
"COMPANY",
|
||||
"Wrong Company",
|
||||
"wrong-company",
|
||||
"wrong-group",
|
||||
),
|
||||
tenant("hanmac-family-id", "COMPANY_GROUP", "한맥가족", "hanmac-family"),
|
||||
tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"),
|
||||
];
|
||||
|
||||
await page.route("**/api/v1/admin/tenants**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
items: tenants,
|
||||
total: tenants.length,
|
||||
limit: 10000,
|
||||
offset: 0,
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/v1/admin/users**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
items: [],
|
||||
total: 0,
|
||||
limit: 5000,
|
||||
offset: 0,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(withShareToken("/picker"));
|
||||
|
||||
const picker = page.frameLocator("iframe");
|
||||
await expect(picker.getByText("한맥가족", { exact: true })).toBeVisible();
|
||||
await expect(picker.getByText("삼안", { exact: true })).toBeVisible();
|
||||
await expect(picker.getByText("Wrong Group", { exact: true })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("picker displays user names with grade and optional position", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(withShareToken("/embed/picker?mode=single&select=user"));
|
||||
|
||||
await expect(
|
||||
page.getByRole("button", {
|
||||
name: "Platform User(Platform Engineer) 책임",
|
||||
name: "Platform User 책임(팀장)",
|
||||
}),
|
||||
).toBeVisible();
|
||||
});
|
||||
@@ -319,17 +370,17 @@ test("embed preview menu updates the iframe picker source", async ({
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("embed preview passes tenant id and custom dimensions through the picker url", async ({
|
||||
test("embed preview passes tenant slug and custom dimensions through the picker url", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(withShareToken("/embed-preview"));
|
||||
|
||||
await page.getByLabel("tenant ID").fill("company-baron");
|
||||
await page.getByLabel("tenant slug").fill("baron");
|
||||
await page.getByLabel("임베딩 너비").fill("520");
|
||||
await page.getByLabel("임베딩 높이").fill("480");
|
||||
|
||||
await expect(page.getByTestId("embed-preview-src")).toContainText(
|
||||
"tenantId=company-baron",
|
||||
"tenantSlug=baron",
|
||||
);
|
||||
await expect(page.getByTestId("embed-preview-src")).toContainText(
|
||||
"width=520",
|
||||
@@ -347,16 +398,16 @@ test("embed preview passes tenant id and custom dimensions through the picker ur
|
||||
await expect(picker.getByText("Sales User")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("embed picker scopes the tree by tenant id, hides users for tenant selection, and keeps direct members before child tenants", async ({
|
||||
test("embed picker scopes the tree by tenant slug, hides users for tenant selection, and keeps direct members before child tenants", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(
|
||||
withShareToken("/embed-preview?tenantId=company-baron&select=tenant"),
|
||||
withShareToken("/embed-preview?tenantSlug=baron&select=tenant"),
|
||||
);
|
||||
|
||||
await expect(page.getByLabel("tenant ID")).toHaveValue("company-baron");
|
||||
await expect(page.getByLabel("tenant slug")).toHaveValue("baron");
|
||||
await expect(page.getByTestId("embed-preview-src")).toContainText(
|
||||
"tenantId=company-baron",
|
||||
"tenantSlug=baron",
|
||||
);
|
||||
|
||||
const picker = page.frameLocator("iframe");
|
||||
@@ -599,7 +650,7 @@ test("embed picker includes descendants by default and can disable descendant in
|
||||
await picker.getByLabel("Engineering 선택").check();
|
||||
await expect(picker.getByLabel("Platform 선택")).toBeChecked();
|
||||
await expect(
|
||||
picker.getByLabel("Platform User(Platform Engineer) 책임 선택"),
|
||||
picker.getByLabel("Platform User 책임(팀장) 선택"),
|
||||
).toBeChecked();
|
||||
await picker.getByRole("button", { name: "선택 완료" }).click();
|
||||
|
||||
@@ -617,7 +668,7 @@ test("embed picker includes descendants by default and can disable descendant in
|
||||
await picker.getByLabel("Engineering 선택").check();
|
||||
await expect(picker.getByLabel("Platform 선택")).not.toBeChecked();
|
||||
await expect(
|
||||
picker.getByLabel("Platform User(Platform Engineer) 책임 선택"),
|
||||
picker.getByLabel("Platform User 책임(팀장) 선택"),
|
||||
).not.toBeChecked();
|
||||
await picker.getByRole("button", { name: "선택 완료" }).click();
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ function user(id: string, name: string, companyCode: string) {
|
||||
role: "user",
|
||||
status: "active",
|
||||
companyCode,
|
||||
position: "사원",
|
||||
grade: "사원",
|
||||
createdAt: "2026-04-01T00:00:00.000Z",
|
||||
updatedAt: "2026-04-01T00:00:00.000Z",
|
||||
};
|
||||
@@ -84,9 +84,7 @@ test("org chart uses svg viewBox zoom for sharp vector rendering", async ({
|
||||
const viewport = page.locator('[data-testid="orgchart-viewport"]');
|
||||
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
||||
await expect(svg).toBeVisible();
|
||||
await expect(
|
||||
svg.locator("text", { hasText: "Engineering User" }),
|
||||
).toBeVisible();
|
||||
await expect(svg.getByText("Engineering User 사원")).toBeVisible();
|
||||
|
||||
const initialViewBox = await svg.getAttribute("viewBox");
|
||||
const transform = await page
|
||||
@@ -142,24 +140,18 @@ test("org chart filters by Hanmac family and company while excluding hanmac.kr a
|
||||
await expect(page.getByText("총 4명")).toBeVisible();
|
||||
|
||||
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
||||
await expect(
|
||||
svg.locator("text", { hasText: "Hidden Hanmac User" }),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
svg.locator("text", { hasText: "Engineering User" }),
|
||||
).toBeVisible();
|
||||
await expect(svg.locator("text", { hasText: "Sales User" })).toBeVisible();
|
||||
await expect(svg.getByText(/Hidden Hanmac User/)).toHaveCount(0);
|
||||
await expect(svg.getByText("Engineering User 사원")).toBeVisible();
|
||||
await expect(svg.getByText("Sales User 사원")).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Baron" }).click();
|
||||
await expect(page.getByText("총 2명")).toBeVisible();
|
||||
await expect(page.getByText("총 4명")).toHaveCount(0);
|
||||
await expect(
|
||||
svg.locator("text", { hasText: "Engineering User" }),
|
||||
).toBeVisible();
|
||||
await expect(svg.locator("text", { hasText: "Sales User" })).toHaveCount(0);
|
||||
await expect(svg.getByText("Engineering User 사원")).toBeVisible();
|
||||
await expect(svg.getByText(/Sales User/)).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("org chart displays user names with job title and position", async ({
|
||||
test("org chart displays user names with grade and optional position", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
||||
@@ -176,7 +168,8 @@ test("org chart displays user names with job title and position", async ({
|
||||
{
|
||||
...user("u-eng", "Engineering User", "engineering"),
|
||||
jobTitle: "Platform Engineer",
|
||||
position: "책임",
|
||||
grade: "책임",
|
||||
position: "팀장",
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -186,11 +179,7 @@ test("org chart displays user names with job title and position", async ({
|
||||
await page.goto("/chart?token=display-name");
|
||||
|
||||
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
||||
await expect(
|
||||
svg.locator("text", {
|
||||
hasText: "Engineering User(Platform Engineer) 책임",
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(svg.getByText("Engineering User 책임(팀장)")).toBeVisible();
|
||||
});
|
||||
|
||||
test("org chart places multi-tenant users only on leaf memberships without duplicate rendering", async ({
|
||||
@@ -313,8 +302,8 @@ test("org chart places multi-tenant users only on leaf memberships without dupli
|
||||
await expect(page.getByText("총 1명")).toBeVisible();
|
||||
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
||||
await expect(svg).toBeVisible();
|
||||
await expect(svg.locator("text", { hasText: "Shared User" })).toHaveCount(1);
|
||||
await expect(svg.locator("text").filter({ hasText: /^1$/ })).toHaveCount(4);
|
||||
await expect(svg.getByText(/Shared User/)).toHaveCount(1);
|
||||
await expect(svg.getByText(/^1$/)).toHaveCount(4);
|
||||
});
|
||||
|
||||
test("org chart counts multi-leaf tenant users once in ancestor totals", async ({
|
||||
@@ -355,8 +344,8 @@ test("org chart counts multi-leaf tenant users once in ancestor totals", async (
|
||||
await expect(page.getByText("총 1명")).toBeVisible();
|
||||
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
||||
await expect(svg).toBeVisible();
|
||||
await expect(svg.locator("text", { hasText: "Shared User" })).toHaveCount(2);
|
||||
await expect(svg.locator("text").filter({ hasText: /^1$/ })).toHaveCount(5);
|
||||
await expect(svg.getByText(/Shared User/)).toHaveCount(2);
|
||||
await expect(svg.getByText(/^1$/)).toHaveCount(5);
|
||||
});
|
||||
|
||||
test("org chart hides system global tenant members", async ({ page }) => {
|
||||
@@ -389,8 +378,8 @@ test("org chart hides system global tenant members", async ({ page }) => {
|
||||
await expect(page.getByText("총 1명")).toBeVisible();
|
||||
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
||||
await expect(svg).toBeVisible();
|
||||
await expect(svg.locator("text", { hasText: "시스템 전역" })).toHaveCount(0);
|
||||
await expect(svg.locator("text", { hasText: "Global Admin" })).toHaveCount(0);
|
||||
await expect(svg.locator("text", { hasText: "System Admin" })).toHaveCount(0);
|
||||
await expect(svg.locator("text", { hasText: "Baron User" })).toBeVisible();
|
||||
await expect(svg.getByText(/시스템 전역/)).toHaveCount(0);
|
||||
await expect(svg.getByText(/Global Admin/)).toHaveCount(0);
|
||||
await expect(svg.getByText(/System Admin/)).toHaveCount(0);
|
||||
await expect(svg.getByText("Baron User 사원")).toBeVisible();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user