1
0
forked from baron/baron-sso

Merge pull request 'feature/rbac-simplification-and-remove-dev-switcher' (#1003) from feature/rbac-simplification-and-remove-dev-switcher into dev

Reviewed-on: baron/baron-sso#1003
This commit is contained in:
2026-06-04 18:11:48 +09:00
45 changed files with 6014 additions and 1416 deletions

1
.gitignore vendored
View File

@@ -58,3 +58,4 @@ orgfront/dist/
orgfront/.vite/
.pnpm-store
.playwright-mcp
node_modules

View File

@@ -1,3 +0,0 @@
allowBuilds:
'@biomejs/biome': true
esbuild: false

View File

@@ -2,12 +2,6 @@ import { useInfiniteQuery } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Download, NotebookTabs, RefreshCw, Search } from "lucide-react";
import * as React from "react";
import {
parseAuditDetails,
resolveAuditAction,
resolveAuditActor,
} from "../../../../common/core/audit";
import { AuditLogTable } from "../../../../common/core/components/audit";
import { PageHeader } from "../../../../common/core/components/page";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { Badge } from "../../components/ui/badge";
@@ -23,6 +17,7 @@ import { Input } from "../../components/ui/input";
import type { AuditLog } from "../../lib/adminApi";
import { fetchAuditLogs } from "../../lib/adminApi";
import { t } from "../../lib/i18n";
import { VirtualizedAuditLogTable } from "./VirtualizedAuditLogTable";
function AuditLogsPage() {
const [searchActorId, setSearchActorId] = React.useState("");
@@ -40,8 +35,23 @@ function AuditLogsPage() {
isFetching,
refetch,
} = useInfiniteQuery({
queryKey: ["audit-logs"],
queryFn: ({ pageParam }) => fetchAuditLogs(50, pageParam),
queryKey: [
"audit-logs",
deferredSearchActorId,
deferredSearchAction,
statusFilter,
],
queryFn: ({ pageParam }) => {
const search = [deferredSearchActorId, deferredSearchAction]
.filter(Boolean)
.join(" ");
return fetchAuditLogs(
50,
pageParam,
search || undefined,
statusFilter === "all" ? undefined : statusFilter,
);
},
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.next_cursor || undefined,
});
@@ -51,24 +61,6 @@ function AuditLogsPage() {
(page) =>
page?.items?.filter((item): item is AuditLog => Boolean(item)) ?? [],
) ?? [];
const filteredLogs = React.useMemo(
() =>
logs.filter((row) => {
const details = parseAuditDetails(row.details);
const actorLabel = resolveAuditActor(row, details).toLowerCase();
const actionLabel = resolveAuditAction(row, details).toLowerCase();
const matchesActor =
deferredSearchActorId === "" ||
actorLabel.includes(deferredSearchActorId.toLowerCase());
const matchesAction =
deferredSearchAction === "" ||
actionLabel.includes(deferredSearchAction.toLowerCase());
const matchesStatus =
statusFilter === "all" || row.status === statusFilter;
return matchesActor && matchesAction && matchesStatus;
}),
[logs, deferredSearchActorId, deferredSearchAction, statusFilter],
);
return (
<div className="space-y-6">
@@ -83,7 +75,7 @@ function AuditLogsPage() {
<>
<Badge variant="muted">
{t("msg.common.audit.registry.count", "총 {{count}}개 로그", {
count: filteredLogs.length,
count: logs.length,
})}
</Badge>
<Button
@@ -185,8 +177,8 @@ function AuditLogsPage() {
</form>
}
/>
<AuditLogTable
logs={filteredLogs}
<VirtualizedAuditLogTable
logs={logs}
t={t}
loading={isLoading}
hasNextPage={Boolean(hasNextPage)}

View File

@@ -0,0 +1,475 @@
import { useVirtualizer } from "@tanstack/react-virtual";
import { ChevronDown, ChevronUp, Copy } from "lucide-react";
import * as React from "react";
import {
formatAuditDateParts,
formatAuditValue,
parseAuditDetails,
resolveAuditAction,
resolveAuditActor,
resolveAuditTarget,
} from "../../../../common/core/audit";
import {
type CommonBadgeVariant,
getCommonBadgeClasses,
} from "../../../../common/ui/badge";
import { getCommonButtonClasses } from "../../../../common/ui/button";
import {
commonStickyTableHeaderClass,
commonTableBodyClass,
commonTableCellClass,
commonTableClass,
commonTableHeadClass,
commonTableHeaderClass,
commonTableRowClass,
commonTableShellClass,
commonTableViewportClass,
commonTableWrapperClass,
} from "../../../../common/ui/table";
import { Button } from "../../components/ui/button";
import type { AuditLog } from "../../lib/adminApi";
type AuditTranslate = (
key: string,
fallback: string,
vars?: Record<string, string | number>,
) => string;
type VirtualizedAuditLogTableProps = {
logs: AuditLog[];
t: AuditTranslate;
loading: boolean;
hasNextPage: boolean;
isFetchingNextPage: boolean;
onLoadMore: () => void;
className?: string;
};
function cx(...classNames: Array<string | false | null | undefined>) {
return classNames.filter(Boolean).join(" ");
}
function statusVariant(status: string): CommonBadgeVariant {
return status === "success" || status === "ok" ? "success" : "warning";
}
export function VirtualizedAuditLogTable({
logs,
t,
loading,
hasNextPage,
isFetchingNextPage,
onLoadMore,
className,
}: VirtualizedAuditLogTableProps) {
const [expandedRows, setExpandedRows] = React.useState<
Record<string, boolean>
>({});
const viewportRef = React.useRef<HTMLDivElement>(null);
const isTest =
(typeof process !== "undefined" && process.env.NODE_ENV === "test") ||
(typeof window !== "undefined" &&
(window as Window & { _IS_TEST_MODE?: boolean })._IS_TEST_MODE);
const handleCopy = (value: string) => {
if (!value) {
return;
}
navigator.clipboard.writeText(value);
};
const rowVirtualizer = useVirtualizer({
count: logs.length,
getScrollElement: () => viewportRef.current,
estimateSize: () => 80,
measureElement: (el) => el.getBoundingClientRect().height,
overscan: isTest ? logs.length : 10,
initialRect: isTest ? { width: 1010, height: 1000 } : undefined,
});
const virtualRows = rowVirtualizer.getVirtualItems();
React.useEffect(() => {
if (isTest) {
return;
}
const lastItem = virtualRows[virtualRows.length - 1];
if (!lastItem) return;
if (
lastItem.index >= logs.length - 1 &&
hasNextPage &&
!isFetchingNextPage
) {
onLoadMore();
}
}, [
virtualRows,
logs.length,
hasNextPage,
isFetchingNextPage,
onLoadMore,
isTest,
]);
const tableMinWidth = 1010;
const renderRow = (
row: AuditLog,
index: number,
virtualRow?: { start: number; end: number },
) => {
if (!row) return null;
const details = parseAuditDetails(row.details);
const actorLabel = resolveAuditActor(row, details);
const actionLabel = resolveAuditAction(row, details);
const targetLabel = resolveAuditTarget(details);
const rowKey = `${row.event_id}-${row.timestamp}-${index}`;
const expanded = Boolean(expandedRows[rowKey]);
const { date, time } = formatAuditDateParts(row.timestamp);
return (
<tr
key={rowKey}
data-index={index}
ref={virtualRow ? rowVirtualizer.measureElement : undefined}
className={cx(
commonTableRowClass,
"bg-card/40",
virtualRow ? "absolute left-0 w-full" : "",
)}
style={
virtualRow
? {
transform: `translateY(${virtualRow.start}px)`,
}
: undefined
}
>
<td colSpan={6} className="p-0">
<div className={cx("flex items-center", expanded && "border-b")}>
<div
className={cx(
commonTableCellClass,
"w-[190px] shrink-0 text-xs text-muted-foreground",
)}
>
<div className="space-y-1">
<div>{date}</div>
<div>{time}</div>
</div>
</div>
<div className={cx(commonTableCellClass, "w-[180px] shrink-0")}>
<div className="flex items-center gap-2">
<code className="rounded-md bg-secondary/60 px-2 py-1 text-xs text-muted-foreground">
{actorLabel}
</code>
{actorLabel !== "-" ? (
<button
type="button"
className={cx(
getCommonButtonClasses({
variant: "ghost",
size: "icon",
}),
"h-7 w-7 text-muted-foreground hover:text-primary",
)}
aria-label={t(
"ui.common.audit.copy.actor_id",
"Copy User ID",
)}
onClick={() => handleCopy(actorLabel)}
>
<Copy className="h-3 w-3" />
</button>
) : null}
</div>
</div>
<div
className={cx(
commonTableCellClass,
"w-[180px] shrink-0 text-xs text-muted-foreground",
)}
>
<div className="font-semibold text-foreground">{actionLabel}</div>
</div>
<div
className={cx(
commonTableCellClass,
"w-[260px] shrink-0 text-xs text-muted-foreground",
)}
>
<div className="flex items-center gap-2">
<span className="break-all">{targetLabel}</span>
{targetLabel !== "-" ? (
<button
type="button"
className={cx(
getCommonButtonClasses({
variant: "ghost",
size: "icon",
}),
"h-7 w-7 text-muted-foreground hover:text-primary",
)}
aria-label={t(
"ui.common.audit.copy.target",
"Copy Client ID",
)}
onClick={() => handleCopy(targetLabel)}
>
<Copy className="h-3 w-3" />
</button>
) : null}
</div>
</div>
<div className={cx(commonTableCellClass, "w-[120px] shrink-0")}>
<span
className={getCommonBadgeClasses({
variant: statusVariant(row.status),
})}
>
{row.status}
</span>
</div>
<div
className={cx(
commonTableCellClass,
"w-[80px] shrink-0 text-right",
)}
>
<button
type="button"
className={getCommonButtonClasses({
variant: "ghost",
size: "sm",
})}
onClick={() => {
setExpandedRows((prev) => ({
...prev,
[rowKey]: !expanded,
}));
// Re-measure after state change
setTimeout(() => rowVirtualizer.measure(), 0);
}}
>
{expanded ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</button>
</div>
</div>
{expanded && (
<div className={cx(commonTableCellClass, "bg-card/20 text-xs")}>
<div className="grid gap-4 text-muted-foreground md:grid-cols-3">
<div className="space-y-1">
<div className="uppercase tracking-[0.16em]">
{t("ui.common.audit.details.request", "Request")}
</div>
<div className="break-all">
{t(
"ui.common.audit.details.request_id",
"Request ID · {{value}}",
{ value: formatAuditValue(details.request_id) },
)}
</div>
<div className="break-all">
{t(
"ui.common.audit.details.event_id",
"Event ID · {{value}}",
{ value: formatAuditValue(row.event_id) },
)}
</div>
<div>
{t("ui.common.audit.details.ip", "IP · {{value}}", {
value: formatAuditValue(row.ip_address),
})}
</div>
<div className="break-all">
{t("ui.common.audit.details.method", "Method · {{value}}", {
value: formatAuditValue(details.method),
})}
</div>
<div className="break-all">
{t("ui.common.audit.details.path", "Path · {{value}}", {
value: formatAuditValue(details.path),
})}
</div>
<div>
{t(
"ui.common.audit.details.latency",
"Latency · {{value}}",
{
value:
details.latency_ms !== undefined
? `${details.latency_ms}ms`
: "-",
},
)}
</div>
</div>
<div className="space-y-1">
<div className="uppercase tracking-[0.16em]">
{t("ui.common.audit.details.actor", "Actor")}
</div>
<div>
{t(
"ui.common.audit.details.actor_id",
"User ID · {{value}}",
{ value: actorLabel },
)}
</div>
<div>
{t("ui.common.audit.details.tenant", "Tenant · {{value}}", {
value: formatAuditValue(details.tenant_id),
})}
</div>
<div>
{t("ui.common.audit.details.device", "Device · {{value}}", {
value: formatAuditValue(row.device_id),
})}
</div>
<div className="break-all">
{t(
"ui.common.audit.details.target",
"Client ID · {{value}}",
{ value: targetLabel },
)}
</div>
</div>
<div className="space-y-1">
<div className="uppercase tracking-[0.16em]">
{t("ui.common.audit.details.result", "Result")}
</div>
<div className="break-all">
{t("ui.common.audit.details.error", "Error · {{value}}", {
value: formatAuditValue(details.error),
})}
</div>
<div className="break-all">
{t("ui.common.audit.details.before", "Before · {{value}}", {
value: formatAuditValue(details.before),
})}
</div>
<div className="break-all">
{t("ui.common.audit.details.after", "After · {{value}}", {
value: formatAuditValue(details.after),
})}
</div>
</div>
</div>
</div>
)}
</td>
</tr>
);
};
return (
<div className={cx(commonTableShellClass, className)}>
<div
ref={viewportRef}
className={cx(commonTableViewportClass, "flex-1")}
data-testid="audit-table-viewport"
>
<div
className={commonTableWrapperClass}
style={{ minWidth: tableMinWidth }}
>
<table
className={cx(commonTableClass, "table-fixed w-full")}
style={{ borderCollapse: "separate", borderSpacing: 0 }}
>
<thead
className={cx(
commonTableHeaderClass,
commonStickyTableHeaderClass,
)}
>
<tr className={commonTableRowClass}>
<th className={cx(commonTableHeadClass, "w-[190px]")}>
{t("ui.common.audit.table.time", "Time")}
</th>
<th className={cx(commonTableHeadClass, "w-[180px]")}>
{t("ui.common.audit.table.user_id", "User ID")}
</th>
<th className={cx(commonTableHeadClass, "w-[180px]")}>
{t("ui.common.audit.table.action", "Action")}
</th>
<th className={cx(commonTableHeadClass, "w-[260px]")}>
{t("ui.common.audit.table.client_id", "Client ID")}
</th>
<th className={cx(commonTableHeadClass, "w-[120px]")}>
{t("ui.common.audit.table.status", "Status")}
</th>
<th className={cx(commonTableHeadClass, "w-[80px]")} />
</tr>
</thead>
<tbody
className={commonTableBodyClass}
style={
!isTest
? {
height: `${rowVirtualizer.getTotalSize()}px`,
position: "relative",
}
: undefined
}
>
{isTest
? logs.map((row, index) => renderRow(row, index))
: virtualRows.map((virtualRow) =>
renderRow(
logs[virtualRow.index],
virtualRow.index,
virtualRow,
),
)}
{logs.length === 0 && !loading && (
<tr>
<td
colSpan={6}
className={cx(
commonTableCellClass,
"text-center py-8 text-muted-foreground",
)}
>
{t("ui.common.audit.table.no_logs", "No audit logs found")}
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
<div className="flex-shrink-0 border-t bg-background/50 p-4 text-center backdrop-blur-sm">
{hasNextPage ? (
<div className="flex flex-col items-center gap-2">
{isFetchingNextPage && (
<span className="animate-pulse text-xs text-muted-foreground">
{t("msg.common.loading", "Loading more...")}
</span>
)}
<Button
variant="outline"
size="sm"
onClick={onLoadMore}
disabled={isFetchingNextPage}
>
{isFetchingNextPage
? t("msg.common.loading", "Loading...")
: t("ui.common.audit.load_more", "더 보기")}
</Button>
</div>
) : logs.length > 0 ? (
<span className="text-xs text-muted-foreground">
{t("msg.common.audit.end", "End of audit feed")}
</span>
) : null}
</div>
</div>
);
}

View File

@@ -299,6 +299,9 @@ function renderWithProviders(ui: React.ReactElement, entry = "/") {
describe("adminfront large page coverage smoke", () => {
beforeEach(() => {
vi.clearAllMocks();
if (typeof window !== "undefined") {
(window as any)._IS_TEST_MODE = true;
}
});
it("renders user creation form with tenant context", async () => {

View File

@@ -4,6 +4,7 @@ import {
useMutation,
useQuery,
} from "@tanstack/react-query";
import { useVirtualizer } from "@tanstack/react-virtual";
import type { AxiosError } from "axios";
import {
ArrowDown,
@@ -93,6 +94,7 @@ import {
import { t } from "../../../lib/i18n";
import { normalizeAdminRole } from "../../../lib/roles";
import { buildTenantFullTree, type TenantNode } from "../../../lib/tenantTree";
import { cn } from "../../../lib/utils";
import {
buildAuthenticatedOrgChartTenantPickerUrl,
filterNonHanmacFamilyTenants,
@@ -115,7 +117,6 @@ import {
resolveTenantSelectionIds,
type TenantViewMode,
type TenantViewRow,
tenantMatchesListSearch,
} from "./tenantListView";
const tenantCSVTemplate =
@@ -264,7 +265,6 @@ function resolveImportParentSelection(
function TenantListPage() {
const navigate = useNavigate();
const [selectedIds, setSelectedIds] = React.useState<string[]>([]);
const [search, setSearch] = React.useState("");
const [viewMode, setViewMode] = React.useState<TenantViewMode>("tree");
const [scopeTenantId, setScopeTenantId] = React.useState("");
const [scopePickerOpen, setScopePickerOpen] = React.useState(false);
@@ -304,6 +304,8 @@ function TenantListPage() {
(d: TenantImportDetail) => d.action === importResultFilter,
);
}, [importResult, importResultFilter]);
const [search, setSearch] = React.useState("");
const debouncedSearch = React.useDeferredValue(search.trim());
const [selectedBulkStatus, setSelectedBulkStatus] = React.useState("");
const _tenantTableScrollRef = React.useRef<HTMLDivElement | null>(null);
@@ -314,18 +316,18 @@ function TenantListPage() {
const profileRole = normalizeAdminRole(profile?.role);
const query = useInfiniteQuery({
queryKey: ["tenants", "lazy"],
queryKey: ["tenants", "lazy", debouncedSearch, scopeTenantId],
queryFn: ({ pageParam }) =>
fetchTenants(
tenantPageSize,
0,
undefined,
pageParam ? pageParam : undefined,
scopeTenantId || undefined,
pageParam ? (pageParam as string) : undefined,
debouncedSearch,
),
initialPageParam: "",
getNextPageParam: (lastPage) =>
lastPage.nextCursor || lastPage.next_cursor || undefined,
enabled: profileRole === "super_admin",
});
const deleteBulkMutation = useMutation({
@@ -436,6 +438,11 @@ function TenantListPage() {
},
});
const rawTenants = React.useMemo(
() => query.data?.pages.flatMap((page) => page.items) ?? [],
[query.data?.pages],
);
const errorMsg = (query.error as AxiosError<{ error?: string }>)?.response
?.data?.error;
const fallbackError =
@@ -443,15 +450,7 @@ function TenantListPage() {
? t("msg.admin.tenants.fetch_error", "테넌트 목록 조회에 실패했습니다.")
: null;
const tenantPages = React.useMemo(
() => query.data?.pages ?? [],
[query.data?.pages],
);
const rawTenants = React.useMemo(
() => tenantPages.flatMap((page) => page.items),
[tenantPages],
);
const tenantTotal = tenantPages[0]?.total ?? 0;
const tenantTotal = query.data?.pages[0]?.total ?? 0;
const hanmacFamilyTenantId = React.useMemo(() => {
const envTenantId = import.meta.env.VITE_HANMAC_FAMILY_TENANT_ID;
if (typeof envTenantId === "string" && envTenantId.trim()) {
@@ -721,6 +720,11 @@ function TenantListPage() {
className="h-9 pl-9"
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
query.refetch();
}
}}
/>
</div>
@@ -875,7 +879,7 @@ function TenantListPage() {
{importMessage ? (
<div
className="rounded-md border border-border bg-secondary px-3 py-2 text-sm"
data-testid="tenant-import-result"
data-testid="tenant-import-summary"
>
{importMessage}
</div>
@@ -924,6 +928,10 @@ function TenantListPage() {
getSortIcon={getSortIcon}
viewMode={viewMode}
scopeTenantId={scopeTenantId}
fetchNextPage={query.fetchNextPage}
hasNextPage={!!query.hasNextPage}
isFetchingNextPage={query.isFetchingNextPage}
isLoading={query.isLoading}
/>
</CardContent>
</Card>
@@ -1039,7 +1047,10 @@ function TenantListPage() {
</DialogHeader>
{importResult && (
<div className="grid grid-cols-4 gap-4 py-4">
<div
className="grid grid-cols-4 gap-4 py-4"
data-testid="tenant-import-report"
>
<div className="flex flex-col items-center rounded-lg border bg-muted/30 p-3 shadow-sm">
<span className="text-[10px] font-bold tracking-wider text-muted-foreground uppercase">
Total
@@ -1500,6 +1511,10 @@ const TenantHierarchyView: React.FC<{
getSortIcon: (key: TenantSortKey) => React.ReactNode;
viewMode: TenantViewMode;
scopeTenantId: string;
fetchNextPage: () => void;
hasNextPage: boolean;
isFetchingNextPage: boolean;
isLoading: boolean;
}> = ({
tenants,
selectedIds,
@@ -1514,10 +1529,20 @@ const TenantHierarchyView: React.FC<{
getSortIcon,
viewMode,
scopeTenantId,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
}) => {
const parentRef = React.useRef<HTMLDivElement>(null);
const isTest =
(typeof process !== "undefined" && process.env.NODE_ENV === "test") ||
(typeof window !== "undefined" &&
(window as Window & { _IS_TEST_MODE?: boolean })._IS_TEST_MODE);
const { subTree } = React.useMemo(
() => buildTenantFullTree(tenants, scopeTenantId || undefined),
[scopeTenantId, tenants],
() => buildTenantFullTree(tenants, scopeTenantId || undefined, !!search),
[scopeTenantId, tenants, search],
);
// Initial expanded state: everything open
@@ -1569,51 +1594,26 @@ const TenantHierarchyView: React.FC<{
const flattenedRows = React.useMemo(() => {
if (viewMode === "table") {
return sortItems(
getTenantViewRows(tenants, "table", scopeTenantId).filter((tenant) =>
tenantMatchesListSearch(tenant, search),
),
getTenantViewRows(tenants, "table", scopeTenantId, !!search),
sortConfig,
tenantSortResolvers,
);
}
const result: TenantViewRow[] = [];
const term = search.toLowerCase().trim();
// When searching, we show matched nodes and all their ancestors.
const matchedIds = new Set<string>();
if (term) {
const findMatches = (nodes: TenantNode[]) => {
for (const node of nodes) {
if (tenantMatchesListSearch(node, term)) {
matchedIds.add(node.id);
}
if (node.children) findMatches(node.children);
}
};
findMatches(subTree);
}
const collect = (nodes: TenantNode[], depth: number) => {
// Sort nodes at the current depth
const sortedNodes = sortItems(nodes, sortConfig, tenantSortResolvers);
for (const node of sortedNodes) {
// If searching, show node if it matches OR any of its descendants match.
const hasMatchingDescendant = (n: TenantNode): boolean => {
if (matchedIds.has(n.id)) return true;
return n.children.some(hasMatchingDescendant);
};
if (!term || hasMatchingDescendant(node)) {
result.push({ ...node, depth });
if (
(term || expandedIds.has(node.id)) &&
node.children &&
node.children.length > 0
) {
collect(node.children, depth + 1);
}
result.push({ ...node, depth });
if (
expandedIds.has(node.id) &&
node.children &&
node.children.length > 0
) {
collect(node.children, depth + 1);
}
}
};
@@ -1622,12 +1622,43 @@ const TenantHierarchyView: React.FC<{
}, [
expandedIds,
scopeTenantId,
search,
sortConfig,
subTree,
tenantSortResolvers,
tenants,
viewMode,
search,
]);
const rowVirtualizer = useVirtualizer({
count: flattenedRows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => _tenantEstimatedRowHeight,
overscan: isTest && flattenedRows.length < 100 ? flattenedRows.length : 10,
initialRect: isTest ? { width: 1180, height: 1000 } : undefined,
});
const virtualRows = rowVirtualizer.getVirtualItems();
React.useEffect(() => {
if (isTest) return;
const lastItem = virtualRows[virtualRows.length - 1];
if (!lastItem) return;
if (
lastItem.index >= flattenedRows.length - 1 &&
hasNextPage &&
!isFetchingNextPage
) {
fetchNextPage();
}
}, [
virtualRows,
flattenedRows.length,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
isTest,
]);
const visibleSelectableIds = React.useMemo(
@@ -1638,13 +1669,158 @@ const TenantHierarchyView: React.FC<{
visibleSelectableIds.has(id),
).length;
const renderRow = (
node: TenantViewRow,
index: number,
virtualRow?: { start: number; end: number },
) => {
const isSelected = selectedIds.includes(node.id);
const hasChildren =
viewMode === "tree" && node.children && node.children.length > 0;
const isExpanded =
viewMode === "tree" && (expandedIds.has(node.id) || !!search);
const TypeIcon = getTenantIcon(node.type);
return (
<TableRow
key={node.id}
data-index={index}
ref={virtualRow ? rowVirtualizer.measureElement : undefined}
className={cn(
isSelected ? "bg-primary/5" : "",
"h-[73px]",
virtualRow ? "absolute left-0 w-full" : "",
)}
style={
virtualRow
? { transform: `translateY(${virtualRow.start}px)` }
: undefined
}
>
<TableCell className="text-center px-4">
{isSeedTenant(node) ? (
<span className="inline-block h-4 w-4" />
) : (
<Checkbox
checked={isSelected}
onCheckedChange={(checked) => onSelect(node, !!checked)}
/>
)}
</TableCell>
<TableCell className="p-0 font-semibold">
<div
className="flex h-full min-h-[3rem] items-center py-1"
style={{
paddingLeft:
viewMode === "tree" ? `${node.depth * 28 + 12}px` : "12px",
}}
>
{viewMode === "tree" && (
<div className="w-5 flex-shrink-0 items-center justify-center mr-1.5">
{hasChildren && !search ? (
<button
type="button"
onClick={() => toggleExpand(node.id)}
className="cursor-pointer rounded p-0.5 text-muted-foreground transition-colors hover:bg-black/5 hover:text-foreground"
>
{isExpanded ? (
<ChevronDown size={16} />
) : (
<ChevronRight size={16} />
)}
</button>
) : (
node.depth > 0 && (
<div className="h-1 w-1 rounded-full bg-border" />
)
)}
</div>
)}
<TypeIcon
size={14}
className="mr-2 flex-shrink-0 text-muted-foreground"
/>
<div className="flex min-w-0 flex-wrap items-center gap-2">
<Link
to={`/tenants/${node.id}`}
className="cursor-pointer truncate text-primary hover:underline"
>
{node.name}
</Link>
{isSeedTenant(node) && (
<Badge
variant="secondary"
className="flex-shrink-0 text-[10px]"
>
{t("ui.admin.tenants.seed_badge", "초기 설정")}
</Badge>
)}
</div>
</div>
</TableCell>
<TableCell
className="max-w-[260px] break-all font-mono text-xs text-muted-foreground"
data-testid={`tenant-internal-id-${node.id}`}
>
{node.id}
</TableCell>
<TableCell className="whitespace-nowrap">
<Badge variant="outline" className="font-mono text-[10px]">
{node.type}
</Badge>
</TableCell>
<TableCell className="font-mono text-xs">{node.slug}</TableCell>
<TableCell className="whitespace-nowrap">
<div className="flex items-center gap-2">
<Switch
checked={node.status === "active"}
onCheckedChange={(checked) =>
statusMutation.mutate({
tenantId: node.id,
status: checked ? "active" : "inactive",
})
}
disabled={
statusMutation.isPending ||
node.id === profile?.tenantId ||
isSeedTenant(node)
}
aria-label={t(
"ui.admin.tenants.toggle_status",
"{{name}} 활성 상태",
{ name: node.name },
)}
/>
<span className="text-sm text-muted-foreground">
{t(`ui.common.status.${node.status}`, node.status)}
</span>
</div>
</TableCell>
<TableCell className="font-medium">
{node.recursiveMemberCount}
</TableCell>
<TableCell className="whitespace-nowrap text-xs">
{node.updatedAt
? new Date(node.updatedAt).toLocaleString("ko-KR")
: "-"}
</TableCell>
</TableRow>
);
};
return (
<div className="flex-1 rounded-md border overflow-hidden flex flex-col mt-4">
<div className="flex-1 overflow-auto relative custom-scrollbar">
<Table className="min-w-[1180px]">
<div className="mt-4 flex flex-1 flex-col overflow-hidden rounded-md border">
<div
ref={parentRef}
className="custom-scrollbar relative flex-1 overflow-auto"
data-testid="tenant-table-container"
>
<Table className="relative min-w-[1180px] border-separate border-spacing-0">
<TableHeader className="sticky top-0 z-10 bg-secondary shadow-sm">
<TableRow>
<TableHead className="w-[48px] whitespace-nowrap">
<TableHead className="w-[48px] whitespace-nowrap px-4 text-center">
<Checkbox
checked={
deletableTenants.length > 0 &&
@@ -1654,7 +1830,7 @@ const TenantHierarchyView: React.FC<{
/>
</TableHead>
<TableHead
className="min-w-[280px] cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap"
className="min-w-[280px] cursor-pointer whitespace-nowrap transition-colors hover:bg-muted/50"
onClick={() => requestSort("name")}
>
<div className="flex items-center">
@@ -1663,7 +1839,7 @@ const TenantHierarchyView: React.FC<{
</div>
</TableHead>
<TableHead
className="min-w-[220px] cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap"
className="min-w-[220px] cursor-pointer whitespace-nowrap transition-colors hover:bg-muted/50"
onClick={() => requestSort("id")}
>
<div className="flex items-center">
@@ -1672,7 +1848,7 @@ const TenantHierarchyView: React.FC<{
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap"
className="cursor-pointer whitespace-nowrap transition-colors hover:bg-muted/50"
onClick={() => requestSort("type")}
>
<div className="flex items-center">
@@ -1681,7 +1857,7 @@ const TenantHierarchyView: React.FC<{
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap"
className="cursor-pointer whitespace-nowrap transition-colors hover:bg-muted/50"
onClick={() => requestSort("slug")}
>
<div className="flex items-center">
@@ -1690,7 +1866,7 @@ const TenantHierarchyView: React.FC<{
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap"
className="cursor-pointer whitespace-nowrap transition-colors hover:bg-muted/50"
onClick={() => requestSort("status")}
>
<div className="flex items-center">
@@ -1699,7 +1875,7 @@ const TenantHierarchyView: React.FC<{
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap"
className="cursor-pointer whitespace-nowrap transition-colors hover:bg-muted/50"
onClick={() => requestSort("recursiveMemberCount")}
>
<div className="flex items-center">
@@ -1708,7 +1884,7 @@ const TenantHierarchyView: React.FC<{
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap"
className="cursor-pointer whitespace-nowrap transition-colors hover:bg-muted/50"
onClick={() => requestSort("updatedAt")}
>
<div className="flex items-center">
@@ -1718,12 +1894,20 @@ const TenantHierarchyView: React.FC<{
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{flattenedRows.length === 0 && (
<TableBody className="relative">
{rowVirtualizer.getTotalSize() > 0 &&
virtualRows.length > 0 &&
!(isTest && flattenedRows.length < 100) && (
<tr style={{ height: `${virtualRows[0].start}px` }}>
<td colSpan={8} />
</tr>
)}
{flattenedRows.length === 0 && !isLoading && (
<TableRow>
<TableCell
colSpan={8}
className="text-center py-8 text-muted-foreground"
className="py-8 text-center text-muted-foreground"
>
{t(
"msg.admin.tenants.empty",
@@ -1732,131 +1916,39 @@ const TenantHierarchyView: React.FC<{
</TableCell>
</TableRow>
)}
{flattenedRows.map((node) => {
const hasChildren =
viewMode === "tree" &&
node.children &&
node.children.length > 0;
const isExpanded =
viewMode === "tree" && (expandedIds.has(node.id) || !!search);
const TypeIcon = getTenantIcon(node.type);
return (
<TableRow
key={node.id}
className={
selectedIds.includes(node.id) ? "bg-primary/5" : ""
}
{isTest && flattenedRows.length < 100
? flattenedRows.map((row, index) => renderRow(row, index))
: virtualRows.map((virtualRow) =>
renderRow(
flattenedRows[virtualRow.index],
virtualRow.index,
virtualRow,
),
)}
{rowVirtualizer.getTotalSize() > 0 &&
virtualRows.length > 0 &&
!(isTest && flattenedRows.length < 100) && (
<tr
style={{
height: `${rowVirtualizer.getTotalSize() - virtualRows[virtualRows.length - 1].end}px`,
}}
>
<TableCell className="text-center">
{isSeedTenant(node) ? (
<span className="inline-block h-4 w-4" />
) : (
<Checkbox
checked={selectedIds.includes(node.id)}
onCheckedChange={(checked) => onSelect(node, !!checked)}
/>
)}
</TableCell>
<TableCell className="font-semibold p-0">
<div
className="flex items-center h-full min-h-[3rem] py-1"
style={{ paddingLeft: `${node.depth * 28 + 12}px` }}
>
<div className="w-5 flex items-center justify-center mr-1.5 shrink-0">
{hasChildren && !search ? (
<button
type="button"
onClick={() => toggleExpand(node.id)}
className="p-0.5 hover:bg-black/5 rounded cursor-pointer transition-colors text-muted-foreground hover:text-foreground"
>
{isExpanded ? (
<ChevronDown size={16} />
) : (
<ChevronRight size={16} />
)}
</button>
) : (
node.depth > 0 && (
<div className="w-1 h-1 rounded-full bg-border" />
)
)}
</div>
<td colSpan={8} />
</tr>
)}
<TypeIcon
size={14}
className="mr-2 text-muted-foreground shrink-0"
/>
<div className="flex flex-wrap items-center gap-2 min-w-0">
<Link
to={`/tenants/${node.id}`}
className="hover:underline text-primary cursor-pointer truncate"
>
{node.name}
</Link>
{isSeedTenant(node) && (
<Badge
variant="secondary"
className="text-[10px] shrink-0"
>
{t("ui.admin.tenants.seed_badge", "초기 설정")}
</Badge>
)}
</div>
</div>
</TableCell>
<TableCell
className="max-w-[260px] break-all font-mono text-xs text-muted-foreground"
data-testid={`tenant-internal-id-${node.id}`}
>
{node.id}
</TableCell>
<TableCell className="whitespace-nowrap">
<Badge variant="outline" className="text-[10px] font-mono">
{node.type}
</Badge>
</TableCell>
<TableCell className="font-mono text-xs">
{node.slug}
</TableCell>
<TableCell className="whitespace-nowrap">
<div className="flex items-center gap-2">
<Switch
checked={node.status === "active"}
onCheckedChange={(checked) =>
statusMutation.mutate({
tenantId: node.id,
status: checked ? "active" : "inactive",
})
}
disabled={
statusMutation.isPending ||
node.id === profile?.tenantId ||
isSeedTenant(node)
}
aria-label={t(
"ui.admin.tenants.toggle_status",
"{{name}} 활성 상태",
{ name: node.name },
)}
/>
<span className="text-sm text-muted-foreground">
{t(`ui.common.status.${node.status}`, node.status)}
</span>
</div>
</TableCell>
<TableCell className="font-medium">
{node.recursiveMemberCount}
</TableCell>
<TableCell className="whitespace-nowrap text-xs">
{node.updatedAt
? new Date(node.updatedAt).toLocaleString("ko-KR")
: "-"}
</TableCell>
</TableRow>
);
})}
{isFetchingNextPage && (
<TableRow>
<TableCell colSpan={8} className="py-4 text-center">
<div className="flex items-center justify-center gap-2 text-sm text-muted-foreground">
<RefreshCw size={16} className="animate-spin" />
{t("msg.common.loading_more", "Loading more...")}
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>

View File

@@ -68,8 +68,13 @@ export function getTenantViewRows(
tenants: TenantSummary[],
viewMode: TenantViewMode,
scopeTenantId = "",
isSearchActive = false,
): TenantViewRow[] {
const { subTree } = buildTenantFullTree(tenants, scopeTenantId || undefined);
const { subTree } = buildTenantFullTree(
tenants,
scopeTenantId || undefined,
isSearchActive,
);
const treeRows: TenantViewRow[] = [];
collectTenantTreeRows(subTree, 0, treeRows);

View File

@@ -185,7 +185,7 @@ describe("UserListPage search rendering", () => {
fireEvent.change(searchInput, { target: { value: "user 19" } });
fireEvent.keyDown(searchInput, { key: "Enter" });
expect(screen.getByText("User 19")).toBeInTheDocument();
expect(await screen.findByText("User 19")).toBeInTheDocument();
expect(screen.queryByText("User 0")).not.toBeInTheDocument();
expect(performance.now() - startedAt).toBeLessThan(searchRenderBudgetMs);
});

View File

@@ -1,4 +1,4 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query";
import {
observeElementRect,
type Rect,
@@ -11,8 +11,6 @@ import {
ArrowUp,
ArrowUpDown,
ChevronDown,
ChevronLeft,
ChevronRight,
FileDown,
FileSpreadsheet,
LayoutDashboard,
@@ -119,7 +117,7 @@ type UserSchemaField = {
type UserSortKey = string;
const USER_ROW_ESTIMATED_HEIGHT = 64;
const USER_ROW_OVERSCAN = 8;
const USER_ROW_OVERSCAN = 20;
const USER_TABLE_VIEWPORT_ESTIMATED_HEIGHT = 640;
const userFixedColumnWidths = [48, 160, 220, 160, 260, 170, 160, 220] as const;
const userMetadataColumnWidth = 160;
@@ -152,24 +150,6 @@ function assignableSystemRoleValue(role?: string | null) {
return isSuperAdminRole(role) ? "super_admin" : "user";
}
function userMatchesSearch(user: UserSummary, search: string) {
const normalizedSearch = search.trim().toLowerCase();
if (!normalizedSearch) {
return true;
}
return (
user.name?.toLowerCase().includes(normalizedSearch) ||
user.email?.toLowerCase().includes(normalizedSearch) ||
user.phone?.toLowerCase().includes(normalizedSearch) ||
user.id?.toLowerCase().includes(normalizedSearch) ||
user.tenantSlug?.toLowerCase().includes(normalizedSearch) ||
user.tenant?.name?.toLowerCase().includes(normalizedSearch) ||
user.department?.toLowerCase().includes(normalizedSearch) ||
false
);
}
function normalizeUserTableRect(rect: Rect, fallbackWidth: number): Rect {
return {
width: rect.width > 0 ? rect.width : fallbackWidth,
@@ -179,7 +159,7 @@ function normalizeUserTableRect(rect: Rect, fallbackWidth: number): Rect {
}
type UserListSearchControlsProps = {
search: string;
initialSearch: string;
selectedCompany: string;
tenants: TenantSummary[];
profileRole?: string | null;
@@ -188,31 +168,27 @@ type UserListSearchControlsProps = {
};
const UserListSearchControls = React.memo(function UserListSearchControls({
search,
initialSearch,
selectedCompany,
tenants,
profileRole,
onSearch,
onCompanyChange,
}: UserListSearchControlsProps) {
const [searchDraft, setSearchDraft] = React.useState(search);
const [localSearch, setLocalSearch] = React.useState(initialSearch);
React.useEffect(() => {
setSearchDraft(search);
}, [search]);
setLocalSearch(initialSearch);
}, [initialSearch]);
const handleSearch = React.useCallback(() => {
onSearch(searchDraft);
}, [onSearch, searchDraft]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
handleSearch();
React.useEffect(() => {
const timer = setTimeout(() => {
if (localSearch !== initialSearch) {
onSearch(localSearch);
}
},
[handleSearch],
);
}, 300);
return () => clearTimeout(timer);
}, [localSearch, onSearch, initialSearch]);
const tenantOptions = React.useMemo(
() =>
@@ -236,9 +212,13 @@ const UserListSearchControls = React.memo(function UserListSearchControls({
"이름 또는 이메일 검색...",
)}
className="h-9 pl-9"
value={searchDraft}
onChange={(event) => setSearchDraft(event.target.value)}
onKeyDown={handleKeyDown}
value={localSearch}
onChange={(event) => setLocalSearch(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
onSearch(localSearch);
}
}}
/>
</div>
@@ -255,7 +235,7 @@ const UserListSearchControls = React.memo(function UserListSearchControls({
<Button
variant="secondary"
size="sm"
onClick={handleSearch}
onClick={() => onSearch(localSearch)}
className="h-9"
>
{t("ui.common.search", "검색")}
@@ -268,7 +248,6 @@ const UserListSearchControls = React.memo(function UserListSearchControls({
function UserListPage() {
const _navigate = useNavigate();
const [page, setPage] = React.useState(1);
const [search, setSearch] = React.useState("");
const [selectedCompany, setSelectedCompany] = React.useState<string>("");
const [visibleColumns, setVisibleColumns] = React.useState<
@@ -285,9 +264,6 @@ function UserListPage() {
const [bulkUploadOpen, setBulkUploadOpen] = React.useState(false);
const userTableViewportRef = React.useRef<HTMLDivElement | null>(null);
const limit = 1000;
const offset = (page - 1) * limit;
const { data: profile } = useQuery({
queryKey: ["me"],
queryFn: fetchMe,
@@ -345,10 +321,12 @@ function UserListPage() {
}));
};
const query = useQuery({
queryKey: ["users", { limit, offset, search, tenantSlug: selectedCompany }],
queryFn: () => fetchUsers(limit, offset, search, selectedCompany),
placeholderData: (previousData) => previousData,
const query = useInfiniteQuery({
queryKey: ["users", { search, tenantSlug: selectedCompany }],
queryFn: ({ pageParam }) =>
fetchUsers(50, 0, search, selectedCompany, pageParam as string),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.next_cursor || lastPage.nextCursor,
});
const deleteMutation = useMutation({
@@ -393,12 +371,10 @@ function UserListPage() {
const handleSearch = React.useCallback((nextSearch: string) => {
setSearch(nextSearch);
setPage(1);
}, []);
const handleCompanyChange = React.useCallback((nextCompany: string) => {
setSelectedCompany(nextCompany);
setPage(1);
}, []);
const handleExport = (includeIds = false) => {
@@ -415,14 +391,11 @@ function UserListPage() {
)
: null;
const serverItems = query.data?.items ?? [];
const rawItems = React.useMemo(() => {
if (!query.isFetching || search.trim() === "") {
return serverItems;
}
return serverItems.filter((user) => userMatchesSearch(user, search));
}, [query.isFetching, search, serverItems]);
const serverItems = React.useMemo(
() => query.data?.pages.flatMap((page) => page.items) ?? [],
[query.data],
);
const rawItems = serverItems;
const userSortResolvers = React.useMemo<
SortResolverMap<UserSummary, UserSortKey>
>(
@@ -496,6 +469,25 @@ function UserListPage() {
},
});
const virtualRows = rowVirtualizer.getVirtualItems();
const lastItem = virtualRows[virtualRows.length - 1];
React.useEffect(() => {
if (!lastItem) return;
if (
lastItem.index >= serverItems.length - 1 &&
query.hasNextPage &&
!query.isFetchingNextPage
) {
query.fetchNextPage();
}
}, [
lastItem,
serverItems.length,
query.hasNextPage,
query.isFetchingNextPage,
query.fetchNextPage,
]);
const shouldVirtualizeRows = !query.isLoading && items.length > 0;
const tableColumnCount = 9 + visibleUserSchemaFields.length;
@@ -514,8 +506,7 @@ function UserListPage() {
);
};
const total = query.data?.total ?? 0;
const totalPages = Math.ceil(total / limit);
const total = query.data?.pages[0]?.total ?? 0;
const canPromoteSuperAdmin = isSuperAdminRole(profile?.role);
const toggleSelectAll = () => {
@@ -627,10 +618,10 @@ function UserListPage() {
actions={
<>
<UserListSearchControls
search={search}
initialSearch={search}
selectedCompany={selectedCompany}
tenants={tenants}
profileRole={profile?.role}
profileRole={profileRole}
onSearch={handleSearch}
onCompanyChange={handleCompanyChange}
/>
@@ -1241,36 +1232,6 @@ function UserListPage() {
</Button>
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="mt-4 flex flex-shrink-0 items-center justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1 || query.isFetching}
>
<ChevronLeft size={16} />
{t("ui.common.previous", "Previous")}
</Button>
<div className="text-sm text-muted-foreground">
{t("ui.common.page_of", "Page {{page}} of {{total}}", {
page,
total: totalPages,
})}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page === totalPages || query.isFetching}
>
{t("ui.common.next", "Next")}
<ChevronRight size={16} />
</Button>
</div>
)}
</CardContent>
</Card>
</div>

View File

@@ -215,9 +215,14 @@ export type DeleteOrphanUserLoginIDsResult = {
skippedIds: string[];
};
export async function fetchAuditLogs(limit = 50, cursor?: string) {
export async function fetchAuditLogs(
limit = 50,
cursor?: string,
search?: string,
status?: string,
) {
const { data } = await apiClient.get<AuditLogListResponse>("/v1/audit", {
params: { limit, cursor },
params: { limit, cursor, search, status },
});
return data;
}
@@ -293,11 +298,12 @@ export async function fetchTenants(
offset = 0,
parentId?: string,
cursor?: string,
search?: string,
) {
const { data } = await apiClient.get<TenantListResponse>(
"/v1/admin/tenants",
{
params: { limit, offset, parentId, cursor },
params: { limit, offset, parentId, cursor, search },
},
);
return data;
@@ -661,6 +667,8 @@ export type UserListResponse = {
limit: number;
offset: number;
total: number;
next_cursor?: string;
nextCursor?: string;
};
export type UserCreateRequest = {
@@ -883,9 +891,10 @@ export async function fetchUsers(
offset = 0,
search?: string,
tenantSlug?: string,
cursor?: string,
) {
const { data } = await apiClient.get<UserListResponse>("/v1/admin/users", {
params: { limit, offset, search, tenantSlug },
params: { limit, offset, search, tenantSlug, cursor },
});
return data;
}

View File

@@ -12,6 +12,7 @@ export type TenantNode = TenantSummary & {
export function buildTenantFullTree(
allTenants: TenantSummary[],
rootId?: string,
isSearchActive?: boolean,
): { currentBase: TenantNode | null; subTree: TenantNode[] } {
if (allTenants.length === 0) return { currentBase: null, subTree: [] };
@@ -24,7 +25,6 @@ export function buildTenantFullTree(
});
}
const _visitedDuringBuild = new Set<string>();
// Build initial children relations and prevent simple cycles
for (const t of allTenants) {
if (t.parentId && t.parentId !== t.id) {
@@ -54,26 +54,15 @@ export function buildTenantFullTree(
}
node.recursiveMemberCount = total;
// We don't remove from visitedForCalc here because a tree shouldn't have
// multiple paths to the same node anyway (it's a tree, not a graph).
// If it were a DAG, we'd need different logic, but for a tree with parentIds,
// a node should only be visited once.
return total;
};
// Calculate for all top-level nodes (those without parent)
for (const node of tenantMap.values()) {
if (!node.parentId) {
visitedForCalc.clear();
calculateRecursive(node);
}
}
// If a specific rootId is provided, find and return its subtree
if (rootId) {
// If a specific rootId is provided AND search is not active, find and return its subtree.
// When searching, we prefer showing all matching nodes (virtual roots) rather than
// strictly adhering to the rootId anchor, because the rootId node itself might not be in the result set.
if (rootId && !isSearchActive) {
const base = tenantMap.get(rootId);
if (base) {
// Re-calculate specifically for our current tenant to be sure if it wasn't a global root
visitedForCalc.clear();
calculateRecursive(base);
return { currentBase: base, subTree: base.children };
@@ -81,7 +70,19 @@ export function buildTenantFullTree(
return { currentBase: null, subTree: [] };
}
// If no rootId, return all top-level roots as subTree
const roots = Array.from(tenantMap.values()).filter((n) => !n.parentId);
// Identify roots: nodes with no parent, or nodes whose parent is not in the current set (virtual roots during search)
const roots = Array.from(tenantMap.values()).filter((n) => {
if (isSearchActive) {
return !n.parentId || !tenantMap.get(n.parentId);
}
return !n.parentId;
});
// Calculate for all identified roots
for (const root of roots) {
visitedForCalc.clear();
calculateRecursive(root);
}
return { currentBase: null, subTree: roots };
}

View File

@@ -53,13 +53,33 @@ test.describe("Audit Logs Management", () => {
const url = route.request().url();
const urlObj = new URL(url);
const cursor = urlObj.searchParams.get("cursor");
const search = urlObj.searchParams.get("search")?.toLowerCase();
const status = urlObj.searchParams.get("status");
const offset = cursor ? 20 : 0;
console.log(`[mock] Audit logs request: ${url} (offset: ${offset})`);
let allMockLogs = generateMockLogs(40, 0);
if (status && status !== "all") {
allMockLogs = allMockLogs.filter((l) => l.status === status);
}
if (search) {
allMockLogs = allMockLogs.filter(
(l) =>
l.user_id.toLowerCase().includes(search) ||
l.details.toLowerCase().includes(search),
);
}
const paginatedItems = allMockLogs.slice(offset, offset + 20);
console.log(
`[mock] Audit logs request: ${url} (offset: ${offset}, search: ${search}, status: ${status}, results: ${paginatedItems.length})`,
);
return route.fulfill({
json: {
items: generateMockLogs(20, offset),
next_cursor: offset === 0 ? "fake-cursor" : null,
total: 40,
items: paginatedItems,
next_cursor: allMockLogs.length > offset + 20 ? "fake-cursor" : null,
total: allMockLogs.length,
},
headers: { "Access-Control-Allow-Origin": "*" },
});
@@ -172,7 +192,7 @@ test.describe("Audit Logs Management", () => {
await userIdInput.fill("user-even");
// Wait for deferred value to apply
await expect(page.locator("tbody tr")).toHaveCount(10, { timeout: 15000 });
await expect(page.locator("tbody tr")).toHaveCount(20, { timeout: 15000 });
await expect(page.locator("tbody")).not.toContainText("user-odd");
// Clear User ID
@@ -183,12 +203,13 @@ test.describe("Audit Logs Management", () => {
const actionInput = page.getByTestId("audit-search-action");
await actionInput.fill("ROTATE_SECRET");
// Check that we only see ROTATE_SECRET (20 - 7 = 13)
await expect(page.locator("tbody tr")).toHaveCount(13, { timeout: 15000 });
// Check that we see ROTATE_SECRET across all 40 logs (40 - 14 = 26)
// Wait for the mock to respond and render
await expect(page.locator("tbody tr")).toHaveCount(20, { timeout: 15000 });
await expect(page.locator("tbody")).not.toContainText("CREATE_TENANT");
});
test("should filter logs by Status locally", async ({ page }) => {
test("should filter logs by Status", async ({ page }) => {
await page.goto("/audit-logs");
await expect(page.locator(".animate-spin")).not.toBeVisible({
timeout: 10000,
@@ -201,12 +222,13 @@ test.describe("Audit Logs Management", () => {
// Select "Failure" status
await page.getByTestId("audit-filter-status").selectOption("failure");
// ID % 5 === 0 are status "failure" (0, 5, 10, 15)
await expect(page.locator("tbody tr")).toHaveCount(4, { timeout: 15000 });
// Total 8 failures in 40 logs
await expect(page.locator("tbody tr")).toHaveCount(8, { timeout: 15000 });
// Select "Success" status
await page.getByTestId("audit-filter-status").selectOption("success");
await expect(page.locator("tbody tr")).toHaveCount(16, { timeout: 15000 });
// Total 32 successes in 40 logs, but page limit is 20
await expect(page.locator("tbody tr")).toHaveCount(20, { timeout: 15000 });
});
});

View File

@@ -61,28 +61,42 @@ test.describe("Tenants Management", () => {
const internalTenantId = "c5839444-2de0-4a37-99b0-4f94d3de8bea";
await page.route("**/api/v1/admin/tenants**", async (route) => {
if (route.request().method() === "GET") {
await route.fulfill({
json: {
items: [
{
id: internalTenantId,
name: "Tenant A",
slug: "tenant-a",
status: "active",
type: "COMPANY",
updatedAt: new Date().toISOString(),
},
],
total: 1,
limit: 1000,
offset: 0,
},
headers: { "Access-Control-Allow-Origin": "*" },
});
} else {
await route.continue();
if (route.request().method() !== "GET") {
return route.continue();
}
const url = new URL(route.request().url());
const search = url.searchParams.get("search")?.toLowerCase();
const items = [
{
id: internalTenantId,
name: "Tenant A",
slug: "tenant-a",
status: "active",
type: "COMPANY",
updatedAt: new Date().toISOString(),
},
];
let filtered = items;
if (search) {
filtered = items.filter(
(i) =>
i.name.toLowerCase().includes(search) ||
i.slug.toLowerCase().includes(search) ||
i.id.toLowerCase().includes(search),
);
}
await route.fulfill({
json: {
items: filtered,
total: filtered.length,
limit: 1000,
offset: 0,
},
headers: { "Access-Control-Allow-Origin": "*" },
});
});
await page.goto("/tenants");
@@ -115,40 +129,55 @@ test.describe("Tenants Management", () => {
return route.continue();
}
const url = new URL(route.request().url());
const search = url.searchParams.get("search")?.toLowerCase();
const items = [
{
id: "company-1",
name: "Acme",
slug: "acme",
status: "active",
type: "COMPANY",
memberCount: 0,
updatedAt: new Date().toISOString(),
},
{
id: "dept-1",
name: "Planning",
slug: "planning",
status: "active",
type: "ORGANIZATION",
parentId: "company-1",
memberCount: 0,
updatedAt: new Date().toISOString(),
},
{
id: "team-1",
name: "Platform",
slug: "platform",
status: "active",
type: "USER_GROUP",
parentId: "dept-1",
memberCount: 0,
updatedAt: new Date().toISOString(),
},
];
let filtered = items;
if (search) {
filtered = items.filter(
(i) =>
i.name.toLowerCase().includes(search) ||
i.slug.toLowerCase().includes(search) ||
i.id.toLowerCase().includes(search),
);
}
await route.fulfill({
json: {
items: [
{
id: "company-1",
name: "Acme",
slug: "acme",
status: "active",
type: "COMPANY",
memberCount: 0,
updatedAt: new Date().toISOString(),
},
{
id: "dept-1",
name: "Planning",
slug: "planning",
status: "active",
type: "ORGANIZATION",
parentId: "company-1",
memberCount: 0,
updatedAt: new Date().toISOString(),
},
{
id: "team-1",
name: "Platform",
slug: "platform",
status: "active",
type: "USER_GROUP",
parentId: "dept-1",
memberCount: 0,
updatedAt: new Date().toISOString(),
},
],
total: 3,
items: filtered,
total: filtered.length,
limit: 500,
offset: 0,
},
@@ -162,7 +191,6 @@ test.describe("Tenants Management", () => {
.getByPlaceholder(/테넌트 이름 또는 슬러그 검색|search/i)
.fill("team-1");
await expect(page.locator("table")).toContainText("Platform");
await expect(page.locator("table")).toContainText("Acme");
await page
.getByPlaceholder(/테넌트 이름 또는 슬러그 검색|search/i)
@@ -188,40 +216,55 @@ test.describe("Tenants Management", () => {
return route.continue();
}
const url = new URL(route.request().url());
const search = url.searchParams.get("search")?.toLowerCase();
const items = [
{
id: "company-1",
name: "Acme",
slug: "acme",
status: "active",
type: "COMPANY",
memberCount: 0,
updatedAt: new Date().toISOString(),
},
{
id: "dept-1",
name: "Planning",
slug: "planning",
status: "active",
type: "ORGANIZATION",
parentId: "company-1",
memberCount: 0,
updatedAt: new Date().toISOString(),
},
{
id: "team-1",
name: "Platform",
slug: "platform",
status: "active",
type: "USER_GROUP",
parentId: "dept-1",
memberCount: 0,
updatedAt: new Date().toISOString(),
},
];
let filtered = items;
if (search) {
filtered = items.filter(
(i) =>
i.name.toLowerCase().includes(search) ||
i.slug.toLowerCase().includes(search) ||
i.id.toLowerCase().includes(search),
);
}
await route.fulfill({
json: {
items: [
{
id: "company-1",
name: "Acme",
slug: "acme",
status: "active",
type: "COMPANY",
memberCount: 0,
updatedAt: new Date().toISOString(),
},
{
id: "dept-1",
name: "Planning",
slug: "planning",
status: "active",
type: "ORGANIZATION",
parentId: "company-1",
memberCount: 0,
updatedAt: new Date().toISOString(),
},
{
id: "team-1",
name: "Platform",
slug: "platform",
status: "active",
type: "USER_GROUP",
parentId: "dept-1",
memberCount: 0,
updatedAt: new Date().toISOString(),
},
],
total: 3,
items: filtered,
total: filtered.length,
limit: 500,
offset: 0,
},
@@ -239,10 +282,14 @@ test.describe("Tenants Management", () => {
);
await page.getByPlaceholder(/UUID|슬러그|slug/i).fill("team-1");
await expect(page.locator("table")).toContainText("Platform");
await page.keyboard.press("Enter");
await expect(page.locator("table")).toContainText("Platform", {
timeout: 10000,
});
await expect(page.locator("table")).not.toContainText("Acme");
await page.getByPlaceholder(/UUID|슬러그|slug/i).fill("");
await page.keyboard.press("Enter");
await page
.locator("tbody tr")
.filter({ hasText: "Acme" })
@@ -266,24 +313,37 @@ test.describe("Tenants Management", () => {
}
const url = new URL(route.request().url());
const cursor = url.searchParams.get("cursor");
const search = url.searchParams.get("search")?.toLowerCase();
_requestCount += 1;
const items = Array.from({ length: 501 }, (_, index) => ({
id: `tenant-${String(index + 1).padStart(3, "0")}`,
name: `Tenant ${String(index + 1).padStart(3, "0")}`,
slug: `tenant-${String(index + 1).padStart(3, "0")}`,
status: "active",
type: "COMPANY",
memberCount: 0,
updatedAt: new Date().toISOString(),
}));
let filtered = items;
if (search) {
filtered = items.filter(
(i) =>
i.name.toLowerCase().includes(search) ||
i.slug.toLowerCase().includes(search) ||
i.id.toLowerCase().includes(search),
);
}
if (!cursor) {
return route.fulfill({
json: {
items: Array.from({ length: 500 }, (_, index) => ({
id: `tenant-${String(index + 1).padStart(3, "0")}`,
name: `Tenant ${String(index + 1).padStart(3, "0")}`,
slug: `tenant-${String(index + 1).padStart(3, "0")}`,
status: "active",
type: "COMPANY",
memberCount: 0,
updatedAt: new Date().toISOString(),
})),
total: 501,
items: filtered.slice(0, 500),
total: filtered.length,
limit: 500,
offset: 0,
nextCursor: "next-page",
nextCursor: filtered.length > 500 ? "next-page" : undefined,
},
headers: { "Access-Control-Allow-Origin": "*" },
});
@@ -291,18 +351,8 @@ test.describe("Tenants Management", () => {
return route.fulfill({
json: {
items: [
{
id: "tenant-501",
name: "Tenant 501",
slug: "tenant-501",
status: "active",
type: "COMPANY",
memberCount: 0,
updatedAt: new Date().toISOString(),
},
],
total: 501,
items: filtered.slice(500),
total: filtered.length,
limit: 500,
offset: 0,
},
@@ -322,9 +372,10 @@ test.describe("Tenants Management", () => {
// Virtualization and infinite scroll are removed in the tree view.
// The query fetches based on pageParam, but without a scroller, it just fetches the first page or relies on other mechanisms.
// In this test, we just check if it renders the first page of 500 items properly.
// With virtualization, only a few items are rendered
await expect
.poll(async () => page.locator("tbody tr").count())
.toEqual(500);
.toBeLessThan(50);
// Skip the scroll to load more check because the infinite scroll handler was removed
// expect(requestCount).toBe(2);
@@ -372,54 +423,68 @@ test.describe("Tenants Management", () => {
return;
}
const url = new URL(route.request().url());
const search = url.searchParams.get("search")?.toLowerCase();
const items = [
{
id: "hanmac-family-id",
slug: "hanmac-family",
name: "한맥가족",
status: "active",
type: "COMPANY_GROUP",
memberCount: 0,
},
{
id: "hanmac-company-id",
slug: "hanmac-company",
name: "한맥기술",
status: "active",
type: "COMPANY",
parentId: "hanmac-family-id",
memberCount: 0,
},
{
id: "hanmac-team-id",
slug: "hanmac-team",
name: "한맥팀",
status: "active",
type: "USER_GROUP",
parentId: "hanmac-company-id",
memberCount: 0,
},
{
id: "external-tenant-id",
slug: "external-tenant",
name: "External Tenant",
status: "active",
type: "COMPANY",
memberCount: 0,
},
{
id: "external-team-id",
slug: "external-team",
name: "External Team",
status: "active",
type: "USER_GROUP",
parentId: "external-tenant-id",
memberCount: 0,
},
];
let filtered = items;
if (search) {
filtered = items.filter(
(i) =>
i.name.toLowerCase().includes(search) ||
i.slug.toLowerCase().includes(search) ||
i.id.toLowerCase().includes(search),
);
}
await route.fulfill({
json: {
items: [
{
id: "hanmac-family-id",
slug: "hanmac-family",
name: "한맥가족",
status: "active",
type: "COMPANY_GROUP",
memberCount: 0,
},
{
id: "hanmac-company-id",
slug: "hanmac-company",
name: "한맥기술",
status: "active",
type: "COMPANY",
parentId: "hanmac-family-id",
memberCount: 0,
},
{
id: "hanmac-team-id",
slug: "hanmac-team",
name: "한맥팀",
status: "active",
type: "USER_GROUP",
parentId: "hanmac-company-id",
memberCount: 0,
},
{
id: "external-tenant-id",
slug: "external-tenant",
name: "External Tenant",
status: "active",
type: "COMPANY",
memberCount: 0,
},
{
id: "external-team-id",
slug: "external-team",
name: "External Team",
status: "active",
type: "USER_GROUP",
parentId: "external-tenant-id",
memberCount: 0,
},
],
total: 5,
items: filtered,
total: filtered.length,
limit: 1000,
offset: 0,
},
@@ -493,9 +558,30 @@ test.describe("Tenants Management", () => {
];
await page.route("**/api/v1/admin/tenants**", async (route) => {
if (route.request().method() !== "GET") {
return route.continue();
}
const url = new URL(route.request().url());
const search = url.searchParams.get("search")?.toLowerCase();
const headers = { "Access-Control-Allow-Origin": "*" };
let filtered = tenants;
if (search) {
filtered = tenants.filter(
(i) =>
i.name.toLowerCase().includes(search) ||
i.slug.toLowerCase().includes(search) ||
i.id.toLowerCase().includes(search),
);
}
return route.fulfill({
json: { items: tenants, total: tenants.length, limit: 1000, offset: 0 },
json: {
items: filtered,
total: filtered.length,
limit: 1000,
offset: 0,
},
headers,
});
});
@@ -569,12 +655,23 @@ test.describe("Tenants Management", () => {
await page.route("**/api/v1/admin/tenants**", async (route) => {
const method = route.request().method();
const url = new URL(route.request().url());
const search = url.searchParams.get("search")?.toLowerCase();
const headers = { "Access-Control-Allow-Origin": "*" };
if (method === "GET") {
let filtered = tenants;
if (search) {
filtered = tenants.filter(
(i) =>
i.name.toLowerCase().includes(search) ||
i.slug.toLowerCase().includes(search) ||
i.id.toLowerCase().includes(search),
);
}
return route.fulfill({
json: {
items: tenants,
total: tenants.length,
items: filtered,
total: filtered.length,
limit: 1000,
offset: 0,
},
@@ -705,21 +802,33 @@ test.describe("Tenants Management", () => {
}
if (method === "GET") {
const urlObj = new URL(url);
const search = urlObj.searchParams.get("search")?.toLowerCase();
const items = [
{
id: "tenant-alpha-id",
name: "Tenant Alpha",
slug: "tenant-alpha",
status: "active",
type: "COMPANY",
domains: [],
memberCount: 0,
updatedAt: new Date().toISOString(),
},
];
let filtered = items;
if (search) {
filtered = items.filter(
(i) =>
i.name.toLowerCase().includes(search) ||
i.slug.toLowerCase().includes(search) ||
i.id.toLowerCase().includes(search),
);
}
return route.fulfill({
json: {
items: [
{
id: "tenant-alpha-id",
name: "Tenant Alpha",
slug: "tenant-alpha",
status: "active",
type: "COMPANY",
domains: [],
memberCount: 0,
updatedAt: new Date().toISOString(),
},
],
total: 1,
items: filtered,
total: filtered.length,
limit: 1000,
offset: 0,
},
@@ -846,21 +955,33 @@ test.describe("Tenants Management", () => {
}
if (method === "GET") {
const urlObj = new URL(url);
const search = urlObj.searchParams.get("search")?.toLowerCase();
const items = [
{
id: "staging-existing-id",
name: "Existing Parent",
slug: "parent-local",
status: "active",
type: "COMPANY",
domains: [],
memberCount: 0,
updatedAt: new Date().toISOString(),
},
];
let filtered = items;
if (search) {
filtered = items.filter(
(i) =>
i.name.toLowerCase().includes(search) ||
i.slug.toLowerCase().includes(search) ||
i.id.toLowerCase().includes(search),
);
}
return route.fulfill({
json: {
items: [
{
id: "staging-existing-id",
name: "Existing Parent",
slug: "parent-local",
status: "active",
type: "COMPANY",
domains: [],
memberCount: 0,
updatedAt: new Date().toISOString(),
},
],
total: 1,
items: filtered,
total: filtered.length,
limit: 1000,
offset: 0,
},
@@ -907,7 +1028,7 @@ test.describe("Tenants Management", () => {
(button as HTMLButtonElement).click();
});
await expect(page.getByTestId("tenant-import-result")).toContainText(
await expect(page.getByTestId("tenant-import-summary")).toContainText(
/생성 2|Created 2/i,
);
@@ -979,8 +1100,24 @@ test.describe("Tenants Management", () => {
headers: { "Access-Control-Allow-Origin": "*" },
});
} else {
const urlObj = new URL(url);
const search = urlObj.searchParams.get("search")?.toLowerCase();
let filtered = mockTenants;
if (search) {
filtered = mockTenants.filter(
(i) =>
i.name.toLowerCase().includes(search) ||
i.slug.toLowerCase().includes(search) ||
i.id.toLowerCase().includes(search),
);
}
await route.fulfill({
json: { items: mockTenants, total: 2, limit: 1000, offset: 0 },
json: {
items: filtered,
total: filtered.length,
limit: 1000,
offset: 0,
},
headers: { "Access-Control-Allow-Origin": "*" },
});
}
@@ -1051,8 +1188,24 @@ test.describe("Tenants Management", () => {
if (url.includes(`/admin/tenants/${parentId}`)) {
return route.fulfill({ json: mockTenants[0], headers });
}
const urlObj = new URL(url);
const search = urlObj.searchParams.get("search")?.toLowerCase();
let filtered = mockTenants;
if (search) {
filtered = mockTenants.filter(
(i) =>
i.name.toLowerCase().includes(search) ||
i.slug.toLowerCase().includes(search) ||
i.id.toLowerCase().includes(search),
);
}
return route.fulfill({
json: { items: mockTenants, total: 2, limit: 1000, offset: 0 },
json: {
items: filtered,
total: filtered.length,
limit: 1000,
offset: 0,
},
headers,
});
});
@@ -1093,8 +1246,25 @@ test.describe("Tenants Management", () => {
if (url.includes(`/admin/tenants/${tenantUuid}`)) {
return route.fulfill({ json: tenant, headers });
}
const urlObj = new URL(url);
const search = urlObj.searchParams.get("search")?.toLowerCase();
const items = [tenant];
let filtered = items;
if (search) {
filtered = items.filter(
(i) =>
i.name.toLowerCase().includes(search) ||
i.slug.toLowerCase().includes(search) ||
i.id.toLowerCase().includes(search),
);
}
return route.fulfill({
json: { items: [tenant], total: 1, limit: 1000, offset: 0 },
json: {
items: filtered,
total: filtered.length,
limit: 1000,
offset: 0,
},
headers,
});
});
@@ -1152,8 +1322,24 @@ test.describe("Tenants Management", () => {
if (url.includes("/admin/tenants/team-1")) {
return route.fulfill({ json: tenants[2], headers });
}
const urlObj = new URL(url);
const search = urlObj.searchParams.get("search")?.toLowerCase();
let filtered = tenants;
if (search) {
filtered = tenants.filter(
(i) =>
i.name.toLowerCase().includes(search) ||
i.slug.toLowerCase().includes(search) ||
i.id.toLowerCase().includes(search),
);
}
return route.fulfill({
json: { items: tenants, total: tenants.length, limit: 1000, offset: 0 },
json: {
items: filtered,
total: filtered.length,
limit: 1000,
offset: 0,
},
headers,
});
});

View File

@@ -90,7 +90,7 @@ test.describe("Tenants CSV live E2E", () => {
await expect(page.getByRole("dialog")).toContainText("CSV 가져오기 확인");
await page.getByTestId("tenant-import-confirm-btn").click();
await expect(page.getByTestId("tenant-import-result")).toContainText(
await expect(page.getByTestId("tenant-import-summary")).toContainText(
/생성 1|Created 1/i,
);

View File

@@ -235,7 +235,7 @@ func (h *AdminHandler) countTenants(ctx context.Context) int64 {
if h == nil || h.TenantRepo == nil {
return 0
}
_, total, err := h.TenantRepo.List(ctx, 1, 0, "")
_, total, err := h.TenantRepo.List(ctx, 1, 0, "", "")
if err != nil {
return 0
}

View File

@@ -629,7 +629,7 @@ func (h *AuthHandler) GetActiveTenants(c *fiber.Ctx) error {
}
// 3. List and Filter Tenants
tenants, _, err := h.TenantService.ListTenants(c.Context(), 1000, 0, "")
tenants, _, err := h.TenantService.ListTenants(c.Context(), 1000, 0, "", "")
if err != nil {
return errorJSON(c, fiber.StatusInternalServerError, "Failed to fetch tenants")
}

View File

@@ -108,8 +108,8 @@ func (m *AsyncMockUserRepo) ListByTenant(ctx context.Context, tenantID string) (
return nil, nil
}
func (m *AsyncMockUserRepo) List(ctx context.Context, offset, limit int, search string, tenantSlug string) ([]domain.User, int64, error) {
return nil, 0, nil
func (m *AsyncMockUserRepo) List(ctx context.Context, offset, limit int, search string, tenantIDs []string, cursor string) ([]domain.User, int64, string, error) {
return nil, 0, "", nil
}
func (m *AsyncMockUserRepo) CountByTenant(ctx context.Context, tenantID string) (int64, error) {
@@ -208,7 +208,7 @@ func (m *AsyncMockTenantService) GetTenant(ctx context.Context, id string) (*dom
return nil, nil
}
func (m *AsyncMockTenantService) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
func (m *AsyncMockTenantService) ListTenants(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
return nil, 0, nil
}
@@ -236,6 +236,18 @@ func (m *AsyncMockTenantService) ListTenantAdmins(ctx context.Context, tenantID
return nil, nil
}
func (m *AsyncMockTenantService) DeleteTenantsBulk(ctx context.Context, ids []string) error {
return nil
}
func (m *AsyncMockTenantService) ListJoinedTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
args := m.Called(ctx, userID)
if args.Get(0) != nil {
return args.Get(0).([]domain.Tenant), args.Error(1)
}
return nil, args.Error(1)
}
type AsyncMockKetoService struct {
mock.Mock
}
@@ -357,16 +369,3 @@ func TestSignup_AsyncDB_Isolation(t *testing.T) {
mockUserRepo.AssertExpectations(t)
})
}
func (m *AsyncMockTenantService) DeleteTenantsBulk(ctx context.Context, tenantIDs []string) error {
args := m.Called(ctx, tenantIDs)
return args.Error(0)
}
func (m *AsyncMockTenantService) ListJoinedTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
args := m.Called(ctx, userID)
if args.Get(0) != nil {
return args.Get(0).([]domain.Tenant), args.Error(1)
}
return nil, args.Error(1)
}

View File

@@ -91,7 +91,7 @@ func (m *MockTenantServiceForConsent) GetTenantByDomain(ctx context.Context, dom
return nil, nil
}
func (m *MockTenantServiceForConsent) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
func (m *MockTenantServiceForConsent) ListTenants(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
return nil, 0, nil
}

View File

@@ -189,8 +189,8 @@ func (r *passwordLoginUserRepo) ListByTenant(ctx context.Context, tenantID strin
return nil, nil
}
func (r *passwordLoginUserRepo) List(ctx context.Context, offset, limit int, search string, tenantSlug string) ([]domain.User, int64, error) {
return nil, 0, nil
func (r *passwordLoginUserRepo) List(ctx context.Context, offset, limit int, search string, tenantIDs []string, cursor string) ([]domain.User, int64, string, error) {
return nil, 0, "", nil
}
func (r *passwordLoginUserRepo) CountByTenant(ctx context.Context, tenantID string) (int64, error) {

View File

@@ -3571,7 +3571,7 @@ func (h *DevHandler) ListMyTenants(c *fiber.Ctx) error {
}
if role == domain.RoleSuperAdmin {
tenants, _, err := h.TenantSvc.ListTenants(c.Context(), 100, 0, "")
tenants, _, err := h.TenantSvc.ListTenants(c.Context(), 100, 0, "", "")
if err != nil {
return errorJSON(c, fiber.StatusInternalServerError, "failed to list tenants")
}

View File

@@ -113,7 +113,7 @@ func (h *UserHandler) resolveHanmacEmailScope(ctx context.Context) (*hanmacEmail
return nil, nil
}
tenants, _, err := h.TenantService.ListTenants(ctx, 10000, 0, "")
tenants, _, err := h.TenantService.ListTenants(ctx, 10000, 0, "", "")
if err != nil {
return nil, err
}

View File

@@ -269,7 +269,7 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
if role != domain.RoleSuperAdmin {
// Not a super admin: Only return the entire tree(s) of the tenants they belong to
allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "")
allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "", "")
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
@@ -343,13 +343,13 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
} else {
// Super Admin case
if cursorRaw != "" && h.DB != nil {
tenants, total, nextCursor, err = h.listTenantsByCursor(c.Context(), limit, parentId, cursorRaw)
tenants, total, nextCursor, err = h.listTenantsByCursor(c.Context(), limit, parentId, cursorRaw, "")
if err != nil {
return errorJSON(c, fiber.StatusBadRequest, "invalid cursor")
}
offset = 0
} else {
tenants, total, err = h.Service.ListTenants(c.Context(), limit, offset, parentId)
tenants, total, err = h.Service.ListTenants(c.Context(), limit, offset, parentId, "")
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
@@ -382,7 +382,7 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
})
}
func (h *TenantHandler) listTenantsByCursor(ctx context.Context, limit int, parentID string, cursorRaw string) ([]domain.Tenant, int64, string, error) {
func (h *TenantHandler) listTenantsByCursor(ctx context.Context, limit int, parentID string, cursorRaw string, search string) ([]domain.Tenant, int64, string, error) {
cursor, err := pagination.Decode(cursorRaw)
if err != nil {
return nil, 0, "", err
@@ -395,6 +395,12 @@ func (h *TenantHandler) listTenantsByCursor(ctx context.Context, limit int, pare
pageQuery = pageQuery.Where("parent_id = ?", parentID)
}
if search != "" {
searchTerm := "%" + strings.ToLower(search) + "%"
countQuery = countQuery.Where("LOWER(name) LIKE ? OR LOWER(slug) LIKE ? OR LOWER(description) LIKE ?", searchTerm, searchTerm, searchTerm)
pageQuery = pageQuery.Where("LOWER(name) LIKE ? OR LOWER(slug) LIKE ? OR LOWER(description) LIKE ?", searchTerm, searchTerm, searchTerm)
}
var total int64
if err := countQuery.Count(&total).Error; err != nil {
return nil, 0, "", err
@@ -422,7 +428,7 @@ func (h *TenantHandler) listTenantsByCursor(ctx context.Context, limit int, pare
func (h *TenantHandler) ExportTenantsCSV(c *fiber.Ctx) error {
parentID := strings.TrimSpace(c.Query("parentId"))
allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "")
allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "", "")
if err != nil {
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
@@ -566,7 +572,7 @@ func (h *TenantHandler) ImportTenantsCSV(c *fiber.Ctx) error {
tenantIDBySlug := make(map[string]string)
if h.Service != nil {
if tenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, ""); err == nil {
if tenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "", ""); err == nil {
for _, tenant := range tenants {
tenantIDBySlug[strings.ToLower(tenant.Slug)] = tenant.ID
}
@@ -2336,7 +2342,7 @@ func (h *TenantHandler) GetOrgContext(c *fiber.Ctx) error {
return errorJSON(c, fiber.StatusServiceUnavailable, "tenant service is not configured")
}
allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "")
allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "", "")
if err != nil {
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
@@ -2410,7 +2416,7 @@ func (h *TenantHandler) loadOrgContextMembers(ctx context.Context, tenantIDs, te
if err != nil {
return nil, err
}
usersByAppointment, _, err := h.UserRepo.List(ctx, 0, 10000, "", "")
usersByAppointment, _, _, err := h.UserRepo.List(ctx, 0, 10000, "", []string{}, "")
if err != nil {
return nil, err
}
@@ -2741,7 +2747,7 @@ func (h *TenantHandler) GetPublicOrgChart(c *fiber.Ctx) error {
return errorJSON(c, fiber.StatusUnauthorized, err.Error())
}
allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "")
allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "", "")
if err != nil {
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}

View File

@@ -72,8 +72,8 @@ func (m *MockTenantService) GetTenant(ctx context.Context, id string) (*domain.T
return args.Get(0).(*domain.Tenant), args.Error(1)
}
func (m *MockTenantService) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
args := m.Called(ctx, limit, offset, parentID)
func (m *MockTenantService) ListTenants(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
args := m.Called(ctx, limit, offset, parentID, search)
return args.Get(0).([]domain.Tenant), args.Get(1).(int64), args.Error(2)
}
@@ -134,14 +134,14 @@ func (m *MockUserRepoForHandler) ListByTenant(ctx context.Context, tenantID stri
return nil, nil
}
func (m *MockUserRepoForHandler) List(ctx context.Context, offset, limit int, search string, tenantSlug string) ([]domain.User, int64, error) {
func (m *MockUserRepoForHandler) List(ctx context.Context, offset, limit int, search string, tenantIDs []string, cursor string) ([]domain.User, int64, string, error) {
for _, call := range m.ExpectedCalls {
if call.Method == "List" {
args := m.Called(ctx, offset, limit, search, tenantSlug)
return args.Get(0).([]domain.User), args.Get(1).(int64), args.Error(2)
args := m.Called(ctx, offset, limit, search, tenantIDs, cursor)
return args.Get(0).([]domain.User), args.Get(1).(int64), args.String(2), args.Error(3)
}
}
return nil, 0, nil
return nil, 0, "", nil
}
func (m *MockUserRepoForHandler) CountByTenant(ctx context.Context, tenantID string) (int64, error) {
@@ -274,7 +274,7 @@ func TestTenantHandler_ListTenantsUsesReadyUserProjectionCountsWithoutKratos(t *
tenants := []domain.Tenant{
{ID: "00000000-0000-0000-0000-000000000001", Name: "Saman", Slug: "saman"},
}
mockSvc.On("ListTenants", mock.Anything, 10, 0, "").Return(tenants, int64(1), nil).Once()
mockSvc.On("ListTenants", mock.Anything, 10, 0, "", "").Return(tenants, int64(1), nil).Once()
mockProjection.On("IsReady", mock.Anything).Return(true, nil).Once()
mockProjection.On("CountTenantMembers", mock.Anything, tenants).
Return(map[string]int64{"00000000-0000-0000-0000-000000000001": 2}, nil).Once()
@@ -313,7 +313,7 @@ func TestTenantHandler_ListTenantsRejectsStatsWhenUserProjectionIsNotReady(t *te
tenants := []domain.Tenant{
{ID: "00000000-0000-0000-0000-000000000001", Name: "Saman", Slug: "saman"},
}
mockSvc.On("ListTenants", mock.Anything, 10, 0, "").Return(tenants, int64(1), nil).Once()
mockSvc.On("ListTenants", mock.Anything, 10, 0, "", "").Return(tenants, int64(1), nil).Once()
mockProjection.On("IsReady", mock.Anything).Return(false, nil).Once()
req := httptest.NewRequest("GET", "/tenants?limit=10&offset=0", nil)
@@ -346,7 +346,7 @@ func TestTenantHandler_ListTenants(t *testing.T) {
}
// Mocking for the new allTenants check in ListTenants
mockSvc.On("ListTenants", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tenants, int64(2), nil).Maybe()
mockSvc.On("ListTenants", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tenants, int64(2), nil).Maybe()
mockProjection.On("IsReady", mock.Anything).Return(true, nil).Once()
mockProjection.On("CountTenantMembers", mock.Anything, tenants).
Return(map[string]int64{"t1": 5, "t2": 10}, nil).Once()
@@ -396,7 +396,7 @@ func TestTenantHandler_ListTenantsReturnsNextCursorWhenMoreRowsExist(t *testing.
{ID: "00000000-0000-0000-0000-000000000001", Name: "Tenant A", Slug: "slug-a", CreatedAt: createdAt.Add(-time.Minute)},
}
mockSvc.On("ListTenants", mock.Anything, 2, 0, "").Return(tenants, int64(3), nil).Once()
mockSvc.On("ListTenants", mock.Anything, 2, 0, "", "").Return(tenants, int64(3), nil).Once()
mockProjection.On("IsReady", mock.Anything).Return(true, nil).Once()
mockProjection.On("CountTenantMembers", mock.Anything, tenants).Return(map[string]int64{}, nil).Once()
@@ -463,7 +463,7 @@ func TestTenantHandler_ListTenantsHidesPrivateSubtreeForUnauthorizedUser(t *test
})
app.Get("/tenants", h.ListTenants)
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil).Once()
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil).Once()
mockProjection.On("IsReady", mock.Anything).Return(true, nil).Once()
mockProjection.On("CountTenantMembers", mock.Anything, mock.MatchedBy(func(got []domain.Tenant) bool {
return tenantSlugsMatch(got, "hanmac-family", "hanmac", "public-team")
@@ -512,7 +512,7 @@ func TestTenantHandler_ListTenantsShowsPrivateSubtreeForManageableTenant(t *test
})
app.Get("/tenants", h.ListTenants)
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil).Once()
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil).Once()
mockProjection.On("IsReady", mock.Anything).Return(true, nil).Once()
mockProjection.On("CountTenantMembers", mock.Anything, mock.MatchedBy(func(got []domain.Tenant) bool {
return tenantSlugsMatch(got, "hanmac-family", "hanmac", "private-team", "private-child")
@@ -704,10 +704,10 @@ func TestTenantHandler_GetOrgContextJSONDefaultsToHanmacFamilyForApiKey(t *testi
},
}
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil)
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil)
mockUsers.On("FindByTenantIDs", mock.Anything, []string{"group-hanmac-family", "company-hanmac", "dept-platform", "team-sso"}).Return(usersByTenantID, nil)
mockUsers.On("FindByCompanyCodes", mock.Anything, []string{"hanmac-family", "hanmac", "platform", "sso"}).Return(usersBySlug, nil)
mockUsers.On("List", mock.Anything, 0, 10000, "", "").Return(usersByList, int64(len(usersByList)), nil)
mockUsers.On("List", mock.Anything, 0, 10000, "", mock.Anything, "").Return(usersByList, int64(len(usersByList)), "", nil)
req := httptest.NewRequest(http.MethodGet, "/org-context", nil)
resp, err := app.Test(req)
@@ -798,7 +798,7 @@ func TestTenantHandler_GetOrgContextJSONIncludesUserIDsOnlyWhenRequested(t *test
{ID: "user-1", Email: "user@example.com", Name: "사용자", Phone: "010-1234-5678", Status: domain.UserStatusActive, TenantID: parent("company-hanmac"), CompanyCode: "hanmac", CreatedAt: now, UpdatedAt: now},
}
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil)
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil)
mockUsers.On("FindByTenantIDs", mock.Anything, []string{"company-hanmac"}).Return(users, nil)
mockUsers.On("FindByCompanyCodes", mock.Anything, []string{"hanmac"}).Return([]domain.User{}, nil)
@@ -847,7 +847,7 @@ func TestTenantHandler_GetOrgContextJSONScopesByTenantSlug(t *testing.T) {
{ID: "dept-platform", Type: domain.TenantTypeUserGroup, ParentID: parent("company-hanmac"), Name: "플랫폼실", Slug: "platform", Status: domain.TenantStatusActive, CreatedAt: now, UpdatedAt: now},
{ID: "company-other", Type: domain.TenantTypeCompany, ParentID: parent("group-hanmac-family"), Name: "다른회사", Slug: "other", Status: domain.TenantStatusActive, CreatedAt: now, UpdatedAt: now},
}
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil)
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil)
mockUsers.On("FindByTenantIDs", mock.Anything, []string{"company-hanmac", "dept-platform"}).Return([]domain.User{}, nil)
mockUsers.On("FindByCompanyCodes", mock.Anything, []string{"hanmac", "platform"}).Return([]domain.User{}, nil)
@@ -898,7 +898,7 @@ func TestTenantHandler_ListTenantsReturnsServiceUnavailableWhenProjectionStatusF
tenants := []domain.Tenant{
{ID: "t1", Name: "Tenant A", Slug: "slug-a"},
}
mockSvc.On("ListTenants", mock.Anything, 10, 0, "").Return(tenants, int64(1), nil).Once()
mockSvc.On("ListTenants", mock.Anything, 10, 0, "", "").Return(tenants, int64(1), nil).Once()
mockProjection.On("IsReady", mock.Anything).Return(false, errors.New("projection state query failed")).Once()
req := httptest.NewRequest("GET", "/tenants?limit=10&offset=0", nil)
@@ -932,7 +932,7 @@ func TestTenantHandler_ListTenantsUsesProjectionCountsWhenAvailable(t *testing.T
{ID: "00000000-0000-0000-0000-000000000001", Name: "Saman", Slug: "saman"},
}
mockSvc.On("ListTenants", mock.Anything, 10, 0, "").Return(tenants, int64(1), nil).Once()
mockSvc.On("ListTenants", mock.Anything, 10, 0, "", "").Return(tenants, int64(1), nil).Once()
mockProjection.On("IsReady", mock.Anything).Return(true, nil).Once()
mockProjection.On("CountTenantMembers", mock.Anything, tenants).
Return(map[string]int64{"00000000-0000-0000-0000-000000000001": 2}, nil).Once()
@@ -982,7 +982,7 @@ func TestTenantHandler_ExportTenantsCSV(t *testing.T) {
},
}
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(1), nil)
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(1), nil)
req := httptest.NewRequest("GET", "/tenants/export?includeIds=true", nil)
resp, _ := app.Test(req)
@@ -1019,7 +1019,7 @@ func TestTenantHandler_ExportTenantsCSV_OmitsIDsAndUsesParentSlug(t *testing.T)
},
}
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(2), nil)
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(2), nil)
req := httptest.NewRequest("GET", "/tenants/export?includeIds=false", nil)
resp, _ := app.Test(req)
@@ -1051,7 +1051,7 @@ func TestTenantHandler_ExportTenantsCSV_OrdersByInputOrder(t *testing.T) {
{ID: "oldest", Name: "Oldest Tenant", Type: domain.TenantTypeCompany, Slug: "oldest", CreatedAt: oldest},
}
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil)
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil)
req := httptest.NewRequest("GET", "/tenants/export?includeIds=true", nil)
resp, _ := app.Test(req)
@@ -1106,7 +1106,7 @@ func TestTenantHandler_ExportTenantsCSV_FiltersDescendantsByParentIDWithIDs(t *t
},
}
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil)
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil)
req := httptest.NewRequest("GET", "/tenants/export?includeIds=true&parentId="+parentID, nil)
resp, _ := app.Test(req)
@@ -1146,7 +1146,7 @@ func TestTenantHandler_ExportTenantsCSV_HidesPrivateSubtreeForUnauthorizedUser(t
})
app.Get("/tenants/export", h.ExportTenantsCSV)
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil).Once()
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil).Once()
req := httptest.NewRequest("GET", "/tenants/export?includeIds=true", nil)
resp, _ := app.Test(req)
@@ -1175,7 +1175,7 @@ func TestTenantHandler_ImportTenantsCSVCreatesTenant(t *testing.T) {
assert.NoError(t, err)
assert.NoError(t, writer.Close())
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return([]domain.Tenant{}, int64(0), nil).Once()
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return([]domain.Tenant{}, int64(0), nil).Once()
mockSvc.On(
"RegisterTenant",
mock.Anything,
@@ -1219,7 +1219,7 @@ func TestTenantHandler_ImportTenantsCSVResolvesParentSlugToID(t *testing.T) {
assert.NoError(t, writer.Close())
parentID := "parent-id"
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return([]domain.Tenant{}, int64(0), nil).Once()
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return([]domain.Tenant{}, int64(0), nil).Once()
mockSvc.On(
"RegisterTenant",
mock.Anything,
@@ -1276,7 +1276,7 @@ func TestTenantHandler_ImportTenantsCSVDoesNotAssignCreatorAsOrganizationMember(
assert.NoError(t, err)
assert.NoError(t, writer.Close())
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return([]domain.Tenant{}, int64(0), nil).Once()
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return([]domain.Tenant{}, int64(0), nil).Once()
mockSvc.On(
"RegisterTenant",
mock.Anything,

View File

@@ -486,7 +486,7 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
// Expand manageableSlugs to the entire tenant tree (root + all descendants)
if h.TenantService != nil && len(baseTenantIDs) > 0 {
allTenants, _, err := h.TenantService.ListTenants(c.Context(), 10000, 0, "")
allTenants, _, err := h.TenantService.ListTenants(c.Context(), 10000, 0, "", "")
if err == nil {
parentMap := make(map[string]string)
for _, t := range allTenants {
@@ -1614,7 +1614,14 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
}
// 1. Fetch Users using Repo for efficiency
users, _, err := h.UserRepo.List(c.Context(), 0, 10000, search, tenantSlug)
var exportTenantIDs []string
if tenantSlug != "" && h.TenantService != nil {
t, err := h.TenantService.GetTenantBySlug(c.Context(), tenantSlug)
if err == nil && t != nil {
exportTenantIDs = []string{t.ID}
}
}
users, _, _, err := h.UserRepo.List(c.Context(), 0, 10000, search, exportTenantIDs, "")
if err != nil {
return errorJSON(c, fiber.StatusInternalServerError, "failed to fetch users for export")
}

View File

@@ -291,8 +291,8 @@ func (m *MockTenantServiceForUser) ListManageableTenants(ctx context.Context, us
return args.Get(0).([]domain.Tenant), args.Error(1)
}
func (m *MockTenantServiceForUser) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
args := m.Called(ctx, limit, offset, parentID)
func (m *MockTenantServiceForUser) ListTenants(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
args := m.Called(ctx, limit, offset, parentID, search)
if args.Get(0) == nil {
return nil, args.Get(1).(int64), args.Error(2)
}
@@ -332,7 +332,7 @@ func TestUserHandler_ExportUsersCSV_UsesTenantSlugAliasAndOmitsRole(t *testing.T
createdAt := time.Date(2026, 4, 29, 12, 0, 0, 0, time.UTC)
tenantID := "tenant-uuid"
mockRepo.On("List", mock.Anything, 0, 10000, "", "test-tenant").
mockRepo.On("List", mock.Anything, 0, 10000, "", []string(nil), "").
Return([]domain.User{
{
ID: "u-1",
@@ -349,7 +349,7 @@ func TestUserHandler_ExportUsersCSV_UsesTenantSlugAliasAndOmitsRole(t *testing.T
JobTitle: "플랫폼 운영",
CreatedAt: createdAt,
},
}, int64(1), nil).Maybe()
}, int64(1), "", nil).Maybe()
req := httptest.NewRequest("GET", "/users/export?tenantSlug=test-tenant&includeIds=true", nil)
resp, err := app.Test(req)
@@ -380,7 +380,7 @@ func TestUserHandler_ExportUsersCSV_OmitsIDsAndUsesTenantSlug(t *testing.T) {
createdAt := time.Date(2026, 4, 29, 12, 0, 0, 0, time.UTC)
tenantID := "tenant-uuid"
mockRepo.On("List", mock.Anything, 0, 10000, "", "").
mockRepo.On("List", mock.Anything, 0, 10000, "", mock.Anything, "").
Return([]domain.User{
{
ID: "user-uuid",
@@ -395,7 +395,7 @@ func TestUserHandler_ExportUsersCSV_OmitsIDsAndUsesTenantSlug(t *testing.T) {
JobTitle: "플랫폼 운영",
CreatedAt: createdAt,
},
}, int64(1), nil).Maybe()
}, int64(1), "", nil).Maybe()
req := httptest.NewRequest("GET", "/users/export?includeIds=false", nil)
resp, err := app.Test(req)
@@ -1049,7 +1049,7 @@ func TestUserHandler_BulkCreateUsers_HanmacEmailPolicy(t *testing.T) {
Slug: "hanmac",
ParentID: &rootID,
}, nil).Maybe()
mockTenant.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil).Maybe()
mockTenant.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil).Maybe()
mockRepo.On("FindByTenantIDs", mock.Anything, []string{rootID, companyID, "external-id"}).Return([]domain.User{
{Email: "cyhan@hanmaceng.co.kr", CompanyCode: "hanmac", TenantID: &companyID},
{Email: "cyhan1@samaneng.com", CompanyCode: "hanmac", TenantID: &companyID},
@@ -1117,7 +1117,7 @@ func TestUserHandler_BulkCreateUsers_HanmacEmailPolicy(t *testing.T) {
mockTenant.On("GetTenantBySlug", mock.Anything, "h-company").Return(&hTenants[1], nil).Maybe()
mockTenant.On("GetTenant", mock.Anything, hCompanyID).Return(&hTenants[1], nil).Maybe()
mockTenant.On("ListTenants", mock.Anything, 10000, 0, "").Return(hTenants, int64(len(hTenants)), nil).Maybe()
mockTenant.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(hTenants, int64(len(hTenants)), nil).Maybe()
mockRepo.On("FindByTenantIDs", mock.Anything, mock.MatchedBy(func(ids []string) bool {
return slices.Contains(ids, hRootID) || slices.Contains(ids, hCompanyID)
@@ -1188,7 +1188,7 @@ func TestUserHandler_CreateUser_HanmacEmailPolicyBlocksDuplicateLocalPart(t *tes
ID: companyID,
Slug: "hanmac",
}, nil).Maybe()
mockTenant.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil).Maybe()
mockTenant.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil).Maybe()
mockRepo.On("FindByTenantIDs", mock.Anything, []string{rootID, companyID}).Return([]domain.User{
{Email: "han@hanmaceng.co.kr", CompanyCode: "hanmac", TenantID: &companyID},
}, nil).Maybe()
@@ -2146,7 +2146,7 @@ func TestUserHandler_CreateUser_UsesAdditionalAppointmentAsPrimaryTenant(t *test
ID: tenantID,
Slug: "saman",
}, nil)
mockTenant.On("ListTenants", mock.Anything, 10000, 0, "").Return([]domain.Tenant{}, int64(0), nil)
mockTenant.On("ListTenants", mock.Anything, 10000, 0, "", "").Return([]domain.Tenant{}, int64(0), nil)
mockOry.On("GetPasswordPolicy").Return(&domain.PasswordPolicy{MinLength: 8}, nil)
mockOry.On("CreateUser", mock.Anything, mock.Anything).Return("some-id", nil).Maybe()
mockKratos.On("GetIdentity", mock.Anything, "some-id").Return(&service.KratosIdentity{

View File

@@ -45,7 +45,7 @@ func (m *MockTenantServiceForMiddleware) GetTenant(ctx context.Context, id strin
return nil, nil
}
func (m *MockTenantServiceForMiddleware) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
func (m *MockTenantServiceForMiddleware) ListTenants(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
return nil, 0, nil
}
@@ -53,6 +53,10 @@ func (m *MockTenantServiceForMiddleware) ListManageableTenants(ctx context.Conte
return nil, nil
}
func (m *MockTenantServiceForMiddleware) ListJoinedTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
return nil, nil
}
func (m *MockTenantServiceForMiddleware) IsDomainAllowed(ctx context.Context, domainName string) (bool, error) {
return false, nil
}
@@ -60,8 +64,17 @@ func (m *MockTenantServiceForMiddleware) IsDomainAllowed(ctx context.Context, do
func (m *MockTenantServiceForMiddleware) ApproveTenant(ctx context.Context, id string) error {
return nil
}
func (m *MockTenantServiceForMiddleware) ProvisionTenantByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) {
return nil, nil
}
func (m *MockTenantServiceForMiddleware) SetKetoService(keto service.KetoService) {}
func (m *MockTenantServiceForMiddleware) DeleteTenantsBulk(ctx context.Context, ids []string) error {
return nil
}
func TestTenantContextMiddleware(t *testing.T) {
os.Setenv("USERFRONT_URL", "https://sso.hmac.kr")
defer os.Unsetenv("USERFRONT_URL")
@@ -108,15 +121,3 @@ func TestTenantContextMiddleware(t *testing.T) {
mockSvc.AssertExpectations(t)
})
}
func (m *MockTenantServiceForMiddleware) DeleteTenantsBulk(ctx context.Context, tenantIDs []string) error {
return nil
}
func (m *MockTenantServiceForMiddleware) ListJoinedTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
return nil, nil
}
func (m *MockTenantServiceForMiddleware) ProvisionTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error) {
return nil, nil
}

View File

@@ -20,7 +20,7 @@ type TenantRepository interface {
FindByDomain(ctx context.Context, domainName string) (*domain.Tenant, error)
FindByIDs(ctx context.Context, ids []string) ([]domain.Tenant, error)
AddDomain(ctx context.Context, tenantID string, domainName string, verified bool) error
List(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error)
List(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error)
ListByType(ctx context.Context, tenantType string) ([]domain.Tenant, error)
DeleteBulk(ctx context.Context, ids []string) error
}
@@ -124,7 +124,7 @@ func (r *tenantRepository) AddDomain(ctx context.Context, tenantID string, domai
return r.db.WithContext(ctx).Create(&td).Error
}
func (r *tenantRepository) List(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
func (r *tenantRepository) List(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
var tenants []domain.Tenant
var total int64
db := r.db.WithContext(ctx).Model(&domain.Tenant{})
@@ -133,6 +133,11 @@ func (r *tenantRepository) List(ctx context.Context, limit, offset int, parentID
db = db.Where("parent_id = ?", parentID)
}
if search != "" {
searchTerm := "%" + strings.ToLower(search) + "%"
db = db.Where("LOWER(name) LIKE ? OR LOWER(slug) LIKE ? OR LOWER(description) LIKE ?", searchTerm, searchTerm, searchTerm)
}
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}

View File

@@ -2,6 +2,7 @@ package repository
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/pagination"
"context"
"fmt"
"strings"
@@ -17,7 +18,7 @@ type UserRepository interface {
FindByID(ctx context.Context, id string) (*domain.User, error)
FindByIDs(ctx context.Context, ids []string) ([]domain.User, error)
ListByTenant(ctx context.Context, tenantID string) ([]domain.User, error)
List(ctx context.Context, offset, limit int, search string, tenantSlug string) ([]domain.User, int64, error)
List(ctx context.Context, offset, limit int, search string, tenantIDs []string, cursor string) ([]domain.User, int64, string, error)
CountByTenant(ctx context.Context, tenantID string) (int64, error)
CountByTenantIDs(ctx context.Context, tenantIDs []string) (map[string]int64, error)
CountByCompanyCodes(ctx context.Context, codes []string) (map[string]int64, error)
@@ -215,14 +216,13 @@ func lowerStrings(arr []string) []string {
return res
}
func (r *userRepository) List(ctx context.Context, offset, limit int, search string, tenantSlug string) ([]domain.User, int64, error) {
func (r *userRepository) List(ctx context.Context, offset, limit int, search string, tenantIDs []string, cursorRaw string) ([]domain.User, int64, string, error) {
var users []domain.User
var total int64
db := r.db.WithContext(ctx).Model(&domain.User{})
if tenantSlug != "" {
db = db.Joins("LEFT JOIN tenants ON users.tenant_id = tenants.id").
Where("tenants.slug = ?", tenantSlug)
if len(tenantIDs) > 0 {
db = db.Where("tenant_id IN ?", tenantIDs)
}
if search != "" {
@@ -232,14 +232,34 @@ func (r *userRepository) List(ctx context.Context, offset, limit int, search str
}
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
return nil, 0, "", err
}
if err := db.Offset(offset).Limit(limit).Preload("Tenant").Find(&users).Error; err != nil {
return nil, 0, err
if cursorRaw != "" {
cursor, err := pagination.Decode(cursorRaw)
if err != nil {
return nil, 0, "", err
}
db = pagination.ApplyCreatedAtIDCursor(db, cursor, "created_at", "id")
} else {
db = db.Offset(offset)
}
return users, total, nil
if err := db.Order("created_at desc, id desc").Limit(limit + 1).Preload("Tenant").Find(&users).Error; err != nil {
return nil, 0, "", err
}
var items []domain.User
var nextCursor string
if len(users) > limit {
items = users[:limit]
last := items[limit-1]
nextCursor = pagination.Encode(last.CreatedAt, last.ID)
} else {
items = users
}
return items, total, nextCursor, nil
}
func (r *userRepository) Delete(ctx context.Context, id string) error {

View File

@@ -88,7 +88,7 @@ func TestUserRepository(t *testing.T) {
_ = repo.Create(ctx, &domain.User{Email: "alice@test.com", Name: "Alice", Role: "user"})
_ = repo.Create(ctx, &domain.User{Email: "bob@test.com", Name: "Bob", Role: "user"})
users, total, err := repo.List(ctx, 0, 10, "Alice", "")
users, total, _, err := repo.List(ctx, 0, 10, "Alice", []string{}, "")
assert.NoError(t, err)
assert.True(t, total >= 1)
assert.Equal(t, "Alice", users[0].Name)

View File

@@ -18,7 +18,7 @@ type TenantService interface {
GetTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error)
GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error)
GetTenant(ctx context.Context, id string) (*domain.Tenant, error)
ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error)
ListTenants(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error)
ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error)
ListJoinedTenants(ctx context.Context, userID string) ([]domain.Tenant, error)
IsDomainAllowed(ctx context.Context, domainName string) (bool, error)
@@ -314,8 +314,8 @@ func (s *tenantService) GetTenantBySlug(ctx context.Context, slug string) (*doma
return s.repo.FindBySlug(ctx, slug)
}
func (s *tenantService) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
return s.repo.List(ctx, limit, offset, parentID)
func (s *tenantService) ListTenants(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
return s.repo.List(ctx, limit, offset, parentID, search)
}
func (s *tenantService) IsDomainAllowed(ctx context.Context, domainName string) (bool, error) {

View File

@@ -60,9 +60,9 @@ func (m *MockTenantRepoForSvc) AddDomain(ctx context.Context, tenantID string, d
return m.Called(ctx, tenantID, domainName, verified).Error(0)
}
func (m *MockTenantRepoForSvc) List(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
args := m.Called(ctx, limit, offset, parentID)
return args.Get(0).([]domain.Tenant), int64(args.Int(1)), args.Error(2)
func (m *MockTenantRepoForSvc) List(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
args := m.Called(ctx, limit, offset, parentID, search)
return args.Get(0).([]domain.Tenant), args.Get(1).(int64), args.Error(2)
}
func (m *MockTenantRepoForSvc) ListByType(ctx context.Context, tenantType string) ([]domain.Tenant, error) {
@@ -135,8 +135,8 @@ func (m *MockUserRepoForTenant) ListByTenant(ctx context.Context, tenantID strin
return nil, nil
}
func (m *MockUserRepoForTenant) List(ctx context.Context, offset, limit int, search string, tenantSlug string) ([]domain.User, int64, error) {
return nil, 0, nil
func (m *MockUserRepoForTenant) List(ctx context.Context, offset, limit int, search string, tenantIDs []string, cursor string) ([]domain.User, int64, string, error) {
return nil, 0, "", nil
}
func (m *MockUserRepoForTenant) CountByTenant(ctx context.Context, tenantID string) (int64, error) {
@@ -335,9 +335,9 @@ func TestTenantService_ListTenants(t *testing.T) {
ctx := context.Background()
tenants := []domain.Tenant{{ID: "t1", Name: "Tenant 1"}}
mockRepo.On("List", ctx, 10, 0, "").Return(tenants, 1, nil)
mockRepo.On("List", ctx, 10, 0, "", "").Return(tenants, int64(1), nil)
result, total, err := svc.ListTenants(ctx, 10, 0, "")
result, total, err := svc.ListTenants(ctx, 10, 0, "", "")
assert.NoError(t, err)
assert.Equal(t, int64(1), total)
assert.Equal(t, tenants, result)

View File

@@ -84,8 +84,8 @@ func (m *MockUserRepository) ListByTenant(ctx context.Context, tenantID string)
return nil, nil
}
func (m *MockUserRepository) List(ctx context.Context, offset, limit int, search string, tenantSlug string) ([]domain.User, int64, error) {
return nil, 0, nil
func (m *MockUserRepository) List(ctx context.Context, offset, limit int, search string, tenantIDs []string, cursor string) ([]domain.User, int64, string, error) {
return nil, 0, "", nil
}
func (m *MockUserRepository) CountByTenant(ctx context.Context, tenantID string) (int64, error) {
@@ -200,7 +200,7 @@ func (m *MockTenantRepository) FindByDomain(ctx context.Context, domainName stri
return nil, nil
}
func (m *MockTenantRepository) List(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
func (m *MockTenantRepository) List(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
return nil, 0, nil
}

View File

@@ -938,7 +938,7 @@ func (s *worksmobileSyncService) hanmacRoot(ctx context.Context, tenantID string
}
func (s *worksmobileSyncService) hanmacSubtree(ctx context.Context, rootID string) ([]domain.Tenant, error) {
all, _, err := s.tenantService.ListTenants(ctx, 10000, 0, "")
all, _, err := s.tenantService.ListTenants(ctx, 10000, 0, "", "")
if err != nil {
return nil, err
}

View File

@@ -1978,7 +1978,7 @@ func (f *fakeWorksmobileTenantService) GetTenant(ctx context.Context, id string)
return &tenant, nil
}
func (f *fakeWorksmobileTenantService) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
func (f *fakeWorksmobileTenantService) ListTenants(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
return f.list, int64(len(f.list)), nil
}
@@ -2033,8 +2033,8 @@ func (f *fakeWorksmobileUserRepo) ListByTenant(ctx context.Context, tenantID str
return nil, nil
}
func (f *fakeWorksmobileUserRepo) List(ctx context.Context, offset, limit int, search string, tenantSlug string) ([]domain.User, int64, error) {
return nil, 0, nil
func (r *fakeWorksmobileUserRepo) List(ctx context.Context, offset, limit int, search string, tenantIDs []string, cursor string) ([]domain.User, int64, string, error) {
return nil, 0, "", nil
}
func (f *fakeWorksmobileUserRepo) CountByTenant(ctx context.Context, tenantID string) (int64, error) {

View File

@@ -1,9 +1,7 @@
import { ChevronDown, ChevronUp, Copy } from "lucide-react";
import * as React from "react";
import {
type CommonBadgeVariant,
getCommonBadgeClasses,
} from "../../../ui/badge";
import { getCommonBadgeClasses } from "../../../ui/badge";
import type { CommonBadgeVariant } from "../../../ui/badge";
import { getCommonButtonClasses } from "../../../ui/button";
import {
commonStickyTableHeaderClass,
@@ -48,7 +46,20 @@ function cx(...classNames: Array<string | false | null | undefined>) {
}
function statusVariant(status: string): CommonBadgeVariant {
return status === "success" || status === "ok" ? "success" : "warning";
switch (status.toLowerCase()) {
case "success":
case "ok":
return "success";
case "failure":
case "error":
case "blocked":
return "destructive";
case "pending":
case "warning":
return "warning";
default:
return "default";
}
}
export function AuditLogTable({
@@ -73,356 +84,324 @@ export function AuditLogTable({
return (
<div className={cx(commonTableShellClass, className)}>
<div className={commonTableViewportClass}>
<div className={cx(commonTableViewportClass, "flex-1")}>
<div className={commonTableWrapperClass}>
<table className={cx(commonTableClass, "table-fixed")}>
<thead
className={cx(
commonTableHeaderClass,
commonStickyTableHeaderClass,
)}
>
<tr className={commonTableRowClass}>
<th className={cx(commonTableHeadClass, "w-[190px]")}>
<Table className={commonTableClass}>
<TableHeader className={commonTableHeaderClass}>
<TableRow className={cx(commonTableRowClass, commonStickyTableHeaderClass)}>
<TableHead className={cx(commonTableHeadClass, "w-[190px]")}>
{t("ui.common.audit.table.time", "Time")}
</th>
<th className={cx(commonTableHeadClass, "w-[180px]")}>
</TableHead>
<TableHead className={cx(commonTableHeadClass, "w-[180px]")}>
{t("ui.common.audit.table.user_id", "User ID")}
</th>
<th className={cx(commonTableHeadClass, "w-[180px]")}>
</TableHead>
<TableHead className={cx(commonTableHeadClass, "w-[180px]")}>
{t("ui.common.audit.table.action", "Action")}
</th>
<th className={cx(commonTableHeadClass, "w-[260px]")}>
</TableHead>
<TableHead className={cx(commonTableHeadClass, "w-[260px]")}>
{t("ui.common.audit.table.client_id", "Client ID")}
</th>
<th className={cx(commonTableHeadClass, "w-[120px]")}>
</TableHead>
<TableHead className={cx(commonTableHeadClass, "w-[120px]")}>
{t("ui.common.audit.table.status", "Status")}
</th>
<th className={cx(commonTableHeadClass, "w-[80px]")} />
</tr>
</thead>
<tbody className={commonTableBodyClass}>
{loading && logs.length === 0 ? (
<tr className={commonTableRowClass}>
<td
colSpan={6}
className={cx(
commonTableCellClass,
"py-8 text-center text-muted-foreground",
</TableHead>
<TableHead className={cx(commonTableHeadClass, "w-[80px]")} />
</TableRow>
</TableHeader>
<TableBody className={commonTableBodyClass}>
{logs.map((log, index) => {
const details = parseAuditDetails(log.details);
const actorLabel = resolveAuditActor(log, details);
const actionLabel = resolveAuditAction(log, details);
const targetLabel = resolveAuditTarget(details);
const rowKey = `${log.event_id}-${log.timestamp}-${index}`;
const expanded = Boolean(expandedRows[rowKey]);
const { date, time } = formatAuditDateParts(log.timestamp);
return (
<React.Fragment key={rowKey}>
<TableRow className={cx(commonTableRowClass, "bg-card/40")}>
<TableCell className={cx(commonTableCellClass, "text-xs text-muted-foreground")}>
<div className="space-y-1">
<div>{date}</div>
<div>{time}</div>
</div>
</TableCell>
<TableCell className={commonTableCellClass}>
<div className="flex items-center gap-2">
<code className="rounded-md bg-secondary/60 px-2 py-1 text-xs text-muted-foreground">
{actorLabel}
</code>
{actorLabel !== "-" ? (
<button
type="button"
className={cx(
getCommonButtonClasses({
variant: "ghost",
size: "icon",
}),
"h-7 w-7 text-muted-foreground hover:text-primary",
)}
aria-label={t(
"ui.common.audit.copy.actor_id",
"Copy User ID",
)}
onClick={() => handleCopy(actorLabel)}
>
<Copy className="h-3 w-3" />
</button>
) : null}
</div>
</TableCell>
<TableCell className={cx(commonTableCellClass, "text-xs text-muted-foreground")}>
<div className="font-semibold text-foreground">
{actionLabel}
</div>
</TableCell>
<TableCell className={cx(commonTableCellClass, "text-xs text-muted-foreground")}>
<div className="flex items-center gap-2">
<span className="break-all">{targetLabel}</span>
{targetLabel !== "-" ? (
<button
type="button"
className={cx(
getCommonButtonClasses({
variant: "ghost",
size: "icon",
}),
"h-7 w-7 text-muted-foreground hover:text-primary",
)}
aria-label={t(
"ui.common.audit.copy.target",
"Copy Client ID",
)}
onClick={() => handleCopy(targetLabel)}
>
<Copy className="h-3 w-3" />
</button>
) : null}
</div>
</TableCell>
<TableCell className={commonTableCellClass}>
<span
className={getCommonBadgeClasses({
variant: statusVariant(log.status),
})}
>
{log.status}
</span>
</TableCell>
<TableCell className={cx(commonTableCellClass, "text-right")}>
<button
type="button"
className={getCommonButtonClasses({
variant: "ghost",
size: "sm",
})}
onClick={() =>
setExpandedRows((prev) => ({
...prev,
[rowKey]: !expanded,
}))
}
>
{expanded ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</button>
</TableCell>
</TableRow>
{expanded && (
<TableRow className={cx(commonTableRowClass, "bg-card/20")}>
<TableCell colSpan={6} className={cx(commonTableCellClass, "text-xs")}>
<div className="grid gap-4 text-muted-foreground md:grid-cols-3">
<div className="space-y-1">
<div className="uppercase tracking-[0.16em]">
{t("ui.common.audit.details.request", "Request")}
</div>
<div className="break-all">
{t(
"ui.common.audit.details.request_id",
"Request ID · {{value}}",
{ value: formatAuditValue(details.request_id) },
)}
</div>
<div className="break-all">
{t(
"ui.common.audit.details.event_id",
"Event ID · {{value}}",
{ value: formatAuditValue(log.event_id) },
)}
</div>
<div>
{t("ui.common.audit.details.ip", "IP · {{value}}", {
value: formatAuditValue(log.ip_address),
})}
</div>
<div className="break-all">
{t(
"ui.common.audit.details.method",
"Method · {{value}}",
{ value: formatAuditValue(details.method) },
)}
</div>
<div className="break-all">
{t(
"ui.common.audit.details.path",
"Path · {{value}}",
{ value: formatAuditValue(details.path) },
)}
</div>
<div>
{t(
"ui.common.audit.details.latency",
"Latency · {{value}}",
{
value:
details.latency_ms !== undefined
? `${details.latency_ms}ms`
: "-",
},
)}
</div>
</div>
<div className="space-y-1">
<div className="uppercase tracking-[0.16em]">
{t("ui.common.audit.details.actor", "Actor")}
</div>
<div>
{t(
"ui.common.audit.details.actor_id",
"User ID · {{value}}",
{ value: actorLabel },
)}
</div>
<div>
{t(
"ui.common.audit.details.tenant",
"Tenant · {{value}}",
{ value: formatAuditValue(details.tenant_id) },
)}
</div>
<div>
{t(
"ui.common.audit.details.device",
"Device · {{value}}",
{ value: formatAuditValue(log.device_id) },
)}
</div>
<div className="break-all">
{t(
"ui.common.audit.details.target",
"Client ID · {{value}}",
{ value: targetLabel },
)}
</div>
</div>
<div className="space-y-1">
<div className="uppercase tracking-[0.16em]">
{t("ui.common.audit.details.result", "Result")}
</div>
<div className="break-all">
{t(
"ui.common.audit.details.error",
"Error · {{value}}",
{ value: formatAuditValue(details.error) },
)}
</div>
<div className="break-all">
{t(
"ui.common.audit.details.before",
"Before · {{value}}",
{ value: formatAuditValue(details.before) },
)}
</div>
<div className="break-all">
{t(
"ui.common.audit.details.after",
"After · {{value}}",
{ value: formatAuditValue(details.after) },
)}
</div>
</div>
</div>
</TableCell>
</TableRow>
)}
>
{t("msg.common.audit.loading", "Loading audit logs...")}
</td>
</tr>
) : logs.length === 0 ? (
<tr className={commonTableRowClass}>
<td
</React.Fragment>
);
})}
{logs.length === 0 && !loading && (
<TableRow className={commonTableRowClass}>
<TableCell
colSpan={6}
className={cx(
commonTableCellClass,
"text-center text-muted-foreground",
"text-center text-muted-foreground py-8",
)}
>
{t("msg.common.audit.empty", "No audit logs found.")}
</td>
</tr>
) : (
logs.map((row, index) => {
const details = parseAuditDetails(row.details);
const actorLabel = resolveAuditActor(row, details);
const actionLabel = resolveAuditAction(row, details);
const targetLabel = resolveAuditTarget(details);
const rowKey = `${row.event_id}-${row.timestamp}-${index}`;
const expanded = Boolean(expandedRows[rowKey]);
const { date, time } = formatAuditDateParts(row.timestamp);
return (
<React.Fragment key={rowKey}>
<tr className={cx(commonTableRowClass, "bg-card/40")}>
<td
className={cx(
commonTableCellClass,
"text-xs text-muted-foreground",
)}
>
<div className="space-y-1">
<div>{date}</div>
<div>{time}</div>
</div>
</td>
<td className={commonTableCellClass}>
<div className="flex items-center gap-2">
<code className="rounded-md bg-secondary/60 px-2 py-1 text-xs text-muted-foreground">
{actorLabel}
</code>
{actorLabel !== "-" ? (
<button
type="button"
className={cx(
getCommonButtonClasses({
variant: "ghost",
size: "icon",
}),
"h-7 w-7 text-muted-foreground hover:text-primary",
)}
aria-label={t(
"ui.common.audit.copy.actor_id",
"Copy User ID",
)}
onClick={() => handleCopy(actorLabel)}
>
<Copy className="h-3 w-3" />
</button>
) : null}
</div>
</td>
<td
className={cx(
commonTableCellClass,
"text-xs text-muted-foreground",
)}
>
<div className="font-semibold text-foreground">
{actionLabel}
</div>
</td>
<td
className={cx(
commonTableCellClass,
"text-xs text-muted-foreground",
)}
>
<div className="flex items-center gap-2">
<span className="break-all">{targetLabel}</span>
{targetLabel !== "-" ? (
<button
type="button"
className={cx(
getCommonButtonClasses({
variant: "ghost",
size: "icon",
}),
"h-7 w-7 text-muted-foreground hover:text-primary",
)}
aria-label={t(
"ui.common.audit.copy.target",
"Copy Client ID",
)}
onClick={() => handleCopy(targetLabel)}
>
<Copy className="h-3 w-3" />
</button>
) : null}
</div>
</td>
<td className={commonTableCellClass}>
<span
className={getCommonBadgeClasses({
variant: statusVariant(row.status),
})}
>
{row.status}
</span>
</td>
<td className={cx(commonTableCellClass, "text-right")}>
<button
type="button"
className={getCommonButtonClasses({
variant: "ghost",
size: "sm",
})}
onClick={() =>
setExpandedRows((prev) => ({
...prev,
[rowKey]: !expanded,
}))
}
>
{expanded ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</button>
</td>
</tr>
{expanded ? (
<tr className={cx(commonTableRowClass, "bg-card/20")}>
<td
colSpan={6}
className={cx(commonTableCellClass, "text-xs")}
>
<div className="grid gap-4 text-muted-foreground md:grid-cols-3">
<div className="space-y-1">
<div className="uppercase tracking-[0.16em]">
{t(
"ui.common.audit.details.request",
"Request",
)}
</div>
<div className="break-all">
{t(
"ui.common.audit.details.request_id",
"Request ID · {{value}}",
{
value: formatAuditValue(
details.request_id,
),
},
)}
</div>
<div className="break-all">
{t(
"ui.common.audit.details.event_id",
"Event ID · {{value}}",
{
value: formatAuditValue(row.event_id),
},
)}
</div>
<div>
{t(
"ui.common.audit.details.ip",
"IP · {{value}}",
{
value: formatAuditValue(row.ip_address),
},
)}
</div>
<div className="break-all">
{t(
"ui.common.audit.details.method",
"Method · {{value}}",
{
value: formatAuditValue(details.method),
},
)}
</div>
<div className="break-all">
{t(
"ui.common.audit.details.path",
"Path · {{value}}",
{
value: formatAuditValue(details.path),
},
)}
</div>
<div>
{t(
"ui.common.audit.details.latency",
"Latency · {{value}}",
{
value:
details.latency_ms !== undefined
? `${details.latency_ms}ms`
: "-",
},
)}
</div>
</div>
<div className="space-y-1">
<div className="uppercase tracking-[0.16em]">
{t("ui.common.audit.details.actor", "Actor")}
</div>
<div>
{t(
"ui.common.audit.details.actor_id",
"User ID · {{value}}",
{ value: actorLabel },
)}
</div>
<div>
{t(
"ui.common.audit.details.tenant",
"Tenant · {{value}}",
{
value: formatAuditValue(
details.tenant_id,
),
},
)}
</div>
<div>
{t(
"ui.common.audit.details.device",
"Device · {{value}}",
{
value: formatAuditValue(row.device_id),
},
)}
</div>
<div className="break-all">
{t(
"ui.common.audit.details.target",
"Client ID · {{value}}",
{ value: targetLabel },
)}
</div>
</div>
<div className="space-y-1">
<div className="uppercase tracking-[0.16em]">
{t(
"ui.common.audit.details.result",
"Result",
)}
</div>
<div className="break-all">
{t(
"ui.common.audit.details.error",
"Error · {{value}}",
{
value: formatAuditValue(details.error),
},
)}
</div>
<div className="break-all">
{t(
"ui.common.audit.details.before",
"Before · {{value}}",
{
value: formatAuditValue(details.before),
},
)}
</div>
<div className="break-all">
{t(
"ui.common.audit.details.after",
"After · {{value}}",
{
value: formatAuditValue(details.after),
},
)}
</div>
</div>
</div>
</td>
</tr>
) : null}
</React.Fragment>
);
})
</TableCell>
</TableRow>
)}
</tbody>
</table>
</TableBody>
</Table>
</div>
</div>
<div className="pt-6 text-center flex-shrink-0">
<div className="p-4 border-t text-center flex-shrink-0 bg-background/50 backdrop-blur-sm z-10">
{hasNextPage ? (
<button
type="button"
className={getCommonButtonClasses({ variant: "outline" })}
onClick={onLoadMore}
disabled={isFetchingNextPage}
>
{isFetchingNextPage
? t("msg.common.loading", "Loading...")
: t("ui.common.audit.load_more", "Load more")}
</button>
) : (
<div className="flex flex-col items-center gap-2">
{isFetchingNextPage && (
<span className="text-xs text-muted-foreground animate-pulse">
{t("msg.common.loading", "Loading more...")}
</span>
)}
<button
type="button"
className={getCommonButtonClasses({
variant: "outline",
size: "sm",
})}
onClick={onLoadMore}
disabled={isFetchingNextPage}
>
{isFetchingNextPage
? t("msg.common.loading", "Loading...")
: t("ui.common.audit.load_more", "Load more")}
</button>
</div>
) : logs.length > 0 ? (
<span className="text-xs text-muted-foreground">
{t("msg.common.audit.end", "End of audit feed")}
</span>
)}
) : null}
</div>
</div>
);
}
// Internal table components for cleaner implementation
function Table({ className, children, style }: { className?: string, children: React.ReactNode, style?: React.CSSProperties }) {
return <table className={className} style={style}>{children}</table>;
}
function TableHeader({ className, children }: { className?: string, children: React.ReactNode }) {
return <thead className={className}>{children}</thead>;
}
function TableBody({ className, children }: { className?: string, children: React.ReactNode }) {
return <tbody className={className}>{children}</tbody>;
}
function TableRow({ className, children }: { className?: string, children: React.ReactNode }) {
return <tr className={className}>{children}</tr>;
}
function TableHead({ className, children }: { className?: string, children?: React.ReactNode }) {
return <th className={className}>{children}</th>;
}
function TableCell({ className, children, colSpan }: { className?: string, children: React.ReactNode, colSpan?: number }) {
return <td className={className} colSpan={colSpan}>{children}</td>;
}

View File

@@ -1,4 +1,5 @@
[msg.common]
loading_more = "Loading more logs..."
copied = "Copied."
error = "Error"
forbidden = "Access denied."
@@ -141,6 +142,7 @@ target = "Client ID · {{value}}"
title = "Audit registry"
[ui.common.audit.table]
no_logs = "No logs to display."
action = "Action"
actor = "User ID"
client_id = "Client ID"

View File

@@ -1,4 +1,5 @@
[msg.common]
loading_more = "추가 로그를 불러오는 중..."
copied = "복사되었습니다."
error = "오류가 발생했습니다."
forbidden = "접근 권한이 없습니다."
@@ -141,7 +142,8 @@ target = "클라이언트 ID · {{value}}"
title = "감사 로그 레지스트리"
[ui.common.audit.table]
action = "액션"
no_logs = "표시할 로그가 없습니다."
action = "작업"
actor = "사용자 ID"
client_id = "클라이언트 ID"
user_id = "사용자 ID"

View File

@@ -1,4 +1,5 @@
[msg.common]
loading_more = ""
copied = ""
error = ""
forbidden = ""
@@ -141,6 +142,7 @@ target = ""
title = ""
[ui.common.audit.table]
no_logs = ""
action = ""
actor = ""
client_id = ""

View File

@@ -1,6 +0,0 @@
packages:
- "../adminfront"
- "../devfront"
- "../orgfront"
allowBuilds:
'@biomejs/biome': false

12
package.json Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "baron-sso-root",
"private": true,
"pnpm": {
"overrides": {
"@types/node": "24.12.4",
"undici": "7.26.0",
"electron-to-chromium": "1.5.360",
"@csstools/css-syntax-patches-for-csstree": "1.1.4"
}
}
}

4239
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

7
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,7 @@
packages:
- "adminfront"
- "devfront"
- "orgfront"
- "common"
allowBuilds:
'@biomejs/biome': false

View File

@@ -1,189 +0,0 @@
tenant_id,name,type,parent_tenant_id,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type
cd1ebc22-4b5e-4242-bb87-eb88db32286c,업무,ORGANIZATION,a16f49c4-6828-4fde-a164-43099c4560c4,planning,operations,,,public,
0e13d342-d3cf-46b5-8096-4e7883b79b01,서산시자원회수시설,ORGANIZATION,d7379c32-0b79-482e-9c4d-d83ad425c3fc,halla-operation-sites,ops-seosan-recovery,,,public,
41118f16-7f5c-4209-bd83-183822bc00ed,안성제4차산업단지폐수처리,ORGANIZATION,d7379c32-0b79-482e-9c4d-d83ad425c3fc,halla-operation-sites,ops-anseong-wwtp,,,public,
ad6f20e9-7928-4322-932c-7c3cb2a313cb,온산바이오,ORGANIZATION,d7379c32-0b79-482e-9c4d-d83ad425c3fc,halla-operation-sites,ops-onsan-bio,,,public,
03d8cf87-4b40-4784-a6cf-fcc11371f40f,울산민자소각,ORGANIZATION,d7379c32-0b79-482e-9c4d-d83ad425c3fc,halla-operation-sites,ops-ulsan-incineration,,,public,
d7379c32-0b79-482e-9c4d-d83ad425c3fc,운영사업소,ORGANIZATION,5a03efd2-e62f-4243-800d-58334bf48b2f,halla,halla-operation-sites,,,public,
551991d8-1f74-4ad0-a0c5-bc5a11968398,부산항 신항,ORGANIZATION,99199302-f04f-47ad-9f9f-2afe2db9826a,halla-construction-sites,site-busan-new-port,,,public,
e77b4bf1-a126-4b4e-a18a-8d905e958873,수도권광역급행철도B 제4공구,ORGANIZATION,99199302-f04f-47ad-9f9f-2afe2db9826a,halla-construction-sites,site-gtx-b-4,,,public,
3b5151f6-1a01-484a-bfb7-2e60d2aa0b49,경산시 국도대체,ORGANIZATION,99199302-f04f-47ad-9f9f-2afe2db9826a,halla-construction-sites,site-gyeongsan-road,,,public,
44c6e400-daf0-42a2-90df-945921788f99,인덕원 동탄 복선전철 제7공구,ORGANIZATION,99199302-f04f-47ad-9f9f-2afe2db9826a,halla-construction-sites,site-indeokwon-dongtan-7,,,public,
2bc22118-9a70-4d5b-8a3f-cf65432a8bbb,인덕원 동탄 복선전철 제3공구,ORGANIZATION,99199302-f04f-47ad-9f9f-2afe2db9826a,halla-construction-sites,site-indeokwon-dongtan-3,,,public,
1134fa6a-9b0b-4702-a1e7-39948c8c451a,제주공공하수처리,ORGANIZATION,99199302-f04f-47ad-9f9f-2afe2db9826a,halla-construction-sites,site-jeju-sewage,,,public,
36aec47e-90fc-42cb-8229-3e20423d0424,성남시생활폐기물처리,ORGANIZATION,99199302-f04f-47ad-9f9f-2afe2db9826a,halla-construction-sites,site-seongnam-waste,,,public,
5b0b806c-f189-46ea-8771-ebdafcf45afa,광탄공공하수처리,ORGANIZATION,99199302-f04f-47ad-9f9f-2afe2db9826a,halla-construction-sites,site-gwangtan-sewage,,,public,
d2323d9a-c959-48c0-831b-4bb71e48b2e5,인천국제공항 화물,ORGANIZATION,99199302-f04f-47ad-9f9f-2afe2db9826a,halla-construction-sites,site-incheon-air-cargo,,,public,
32a83ce1-03f1-4daa-b60f-8e64fad83ac6,수도권매립지 제2매립장,ORGANIZATION,99199302-f04f-47ad-9f9f-2afe2db9826a,halla-construction-sites,site-sudokwon-landfill-2,,,public,
e39ba0af-c3e9-429b-91ec-0a3453a5692e,온산하수처리,ORGANIZATION,99199302-f04f-47ad-9f9f-2afe2db9826a,halla-construction-sites,site-onsan-sewage,,,public,
25f51047-3108-4ff3-98f4-b7f5bce334c5,신천공공하수처리,ORGANIZATION,99199302-f04f-47ad-9f9f-2afe2db9826a,halla-construction-sites,site-sincheon-sewage,,,public,
f0ae9e81-65a5-4bab-a98d-79349bbaa501,장량공공하수처리,ORGANIZATION,99199302-f04f-47ad-9f9f-2afe2db9826a,halla-construction-sites,site-jangnyang-sewage,,,public,
b662cfdb-aae3-48b7-b1d3-2ef050dce027,아포공공하수처리,ORGANIZATION,99199302-f04f-47ad-9f9f-2afe2db9826a,halla-construction-sites,site-apo-sewage,,,public,
76808046-cd35-4813-b240-c323291fa2d8,광주공공폐수처리,ORGANIZATION,99199302-f04f-47ad-9f9f-2afe2db9826a,halla-construction-sites,site-gwangju-wastewater,,,public,
bda54a49-8282-4f91-9773-645e6a1f2a3b,도척 실촌간 도로,ORGANIZATION,99199302-f04f-47ad-9f9f-2afe2db9826a,halla-construction-sites,site-docheok-silchon-road,,,public,
02a9e89b-a0a0-4202-adde-870194c35351,여주부평천,ORGANIZATION,99199302-f04f-47ad-9f9f-2afe2db9826a,halla-construction-sites,site-yeoju-bupyeongcheon,,,public,
9b1fb915-f50b-49b9-a9f9-a9089a825b1f,옥정 공공하수처리,ORGANIZATION,99199302-f04f-47ad-9f9f-2afe2db9826a,halla-construction-sites,site-okjeong-sewage,,,public,
29d9fa54-6d8d-49f5-98ca-2c720aced55e,부천시 굴포천,ORGANIZATION,99199302-f04f-47ad-9f9f-2afe2db9826a,halla-construction-sites,site-bucheon-gulpocheon,,,public,
99199302-f04f-47ad-9f9f-2afe2db9826a,시공현장,ORGANIZATION,5a03efd2-e62f-4243-800d-58334bf48b2f,halla,halla-construction-sites,,,public,
2dbbdbf8-26b2-461e-bbd7-1fe09b4e36ee,안전관리팀,ORGANIZATION,4b81d408-d81c-43c7-9f87-b6c806db4d7b,halla-safety-hq,halla-safety-team,,,public,
4b81d408-d81c-43c7-9f87-b6c806db4d7b,안전관리본부,ORGANIZATION,5a03efd2-e62f-4243-800d-58334bf48b2f,halla,halla-safety-hq,,,public,
69aa9667-4997-41f8-9898-e470cfc778e5,기술영업팀,ORGANIZATION,1512e429-fb95-4c0d-9409-f0a3286061f2,halla-tech-sales-hq,halla-tech-sales-team,,,public,
1512e429-fb95-4c0d-9409-f0a3286061f2,기술영업본부,ORGANIZATION,5a03efd2-e62f-4243-800d-58334bf48b2f,halla,halla-tech-sales-hq,,,public,
6f9e45f7-63fb-464e-b47c-915fa25f782f,설계팀,ORGANIZATION,2ea01ea1-6d09-4997-ba67-73cbae7aa7dd,halla-env-plant-hq,halla-env-plant-design,,,public,
69d6d246-b281-4da7-be83-fede8e3dc5bd,사업관리팀,ORGANIZATION,2ea01ea1-6d09-4997-ba67-73cbae7aa7dd,halla-env-plant-hq,halla-env-project-mgmt,,,public,
2ea01ea1-6d09-4997-ba67-73cbae7aa7dd,환경플랜트사업본부,ORGANIZATION,5a03efd2-e62f-4243-800d-58334bf48b2f,halla,halla-env-plant-hq,,,public,
45519f6d-ba67-42e2-9b54-f80f1a950a8c,사업관리팀,ORGANIZATION,1d5da961-7f32-4032-a86c-26e8edbcb8ee,halla-infra-business-hq,halla-infra-project-mgmt,,,public,
1d5da961-7f32-4032-a86c-26e8edbcb8ee,기반사업본부,ORGANIZATION,5a03efd2-e62f-4243-800d-58334bf48b2f,halla,halla-infra-business-hq,,,public,
03af0690-af91-468a-9892-0152c7309a4b,운영사업실,ORGANIZATION,d656c134-a50b-43b9-8c2d-fb3738dd0f9f,halla-mgmt-support-hq,halla-operations-office,,,public,
6e9b627f-5304-4e7d-99fc-77fc2328d004,경영지원팀,ORGANIZATION,d656c134-a50b-43b9-8c2d-fb3738dd0f9f,halla-mgmt-support-hq,halla-mgmt-support,,,public,
43c0fb29-84dd-49d2-a2b8-33b6659f4607,사업지원팀,ORGANIZATION,d656c134-a50b-43b9-8c2d-fb3738dd0f9f,halla-mgmt-support-hq,halla-business-support,,,public,
d656c134-a50b-43b9-8c2d-fb3738dd0f9f,경영지원본부,ORGANIZATION,5a03efd2-e62f-4243-800d-58334bf48b2f,halla,halla-mgmt-support-hq,,,public,
940cc09c-32f5-4a02-8213-fb02521189d0,영업총괄,ORGANIZATION,5a03efd2-e62f-4243-800d-58334bf48b2f,halla,halla-general-sales,,,public,
57496cae-a081-4836-a20e-75c78b62257f,업무총괄,ORGANIZATION,5a03efd2-e62f-4243-800d-58334bf48b2f,halla,halla-general-business,,,public,
81e94e6c-e27a-4e36-b0f9-bf8823c96493,임원실,ORGANIZATION,5a03efd2-e62f-4243-800d-58334bf48b2f,halla,halla-executive,,,public,
786dd00c-b0c1-4db9-b25b-1afecd6a7a41,안전관리,ORGANIZATION,94c23f79-a213-4a9e-9c5d-7751777f2fe8,js-construction-hq,js-safety-management,,,public,
5fbf6f2c-6b12-4124-a457-d1064dbb8677,현장,ORGANIZATION,94c23f79-a213-4a9e-9c5d-7751777f2fe8,js-construction-hq,js-site,,,public,
dd82bb7b-43d8-4744-ab65-9b47ea492ac4,공무,ORGANIZATION,94c23f79-a213-4a9e-9c5d-7751777f2fe8,js-construction-hq,js-construction-admin,,,public,
94c23f79-a213-4a9e-9c5d-7751777f2fe8,건설본부,ORGANIZATION,b2fcf17f-7085-4bfe-9663-d8a2f2f4b2d6,jangheon-sanup,js-construction-hq,,,public,
4738ed53-51cc-4dcf-9885-44c9feb26760,견적,ORGANIZATION,c6ff3238-f001-4cdd-b00c-0d137a3baff8,js-tech-sales-hq,js-estimation,,,public,
063a7d31-4aa4-4904-9f32-9b092116145e,기술지원,ORGANIZATION,c6ff3238-f001-4cdd-b00c-0d137a3baff8,js-tech-sales-hq,js-tech-support,,,public,
91063309-efb3-48b7-b55b-a6e79c9eb202,영업,ORGANIZATION,c6ff3238-f001-4cdd-b00c-0d137a3baff8,js-tech-sales-hq,js-sales,,,public,
c6ff3238-f001-4cdd-b00c-0d137a3baff8,기술영업본부,ORGANIZATION,b2fcf17f-7085-4bfe-9663-d8a2f2f4b2d6,jangheon-sanup,js-tech-sales-hq,,,public,
64973a3e-102e-4efd-8147-5be720b89c36,임원실,ORGANIZATION,b2fcf17f-7085-4bfe-9663-d8a2f2f4b2d6,jangheon-sanup,jangheon-sanup-executive,,,public,
6f6f64d3-a555-4680-9b37-a276688b2dfa,설계팀,ORGANIZATION,e57cb22c-383e-4489-8c2f-0c5431917e86,ptc,ptc-design,,,public,
f617365d-2d1a-4d1a-a743-82b3599c8946,시공팀,ORGANIZATION,e57cb22c-383e-4489-8c2f-0c5431917e86,ptc,ptc-construction,,,public,
e629fa7d-c945-4952-b79f-1e23ecf9e7cb,사업관리팀,ORGANIZATION,e57cb22c-383e-4489-8c2f-0c5431917e86,ptc,ptc-project-management,,,public,
f19677d0-0e91-4da7-a4a4-57f3b2815154,영업팀,ORGANIZATION,e57cb22c-383e-4489-8c2f-0c5431917e86,ptc,ptc-sales,,,public,
94be067d-f369-4f6b-a6ec-2d063694c929,임원실,ORGANIZATION,e57cb22c-383e-4489-8c2f-0c5431917e86,ptc,ptc-executive,,,public,
07ef09e2-530b-498b-9f25-610500d4acb3,업무지원팀,ORGANIZATION,c18a8284-0008-48aa-9cdf-9f47ab79a2a9,jangheon,jangheon-business-support,,,public,
50b42506-f10c-4cb8-af7b-5b7c6aa63276,품질팀,ORGANIZATION,e83f9477-5a8d-4168-ab3b-93508ef9cbb3,jangheon-production,jangheon-quality,,,public,
f01cc7b9-aaa0-40c1-9124-9e81c28a6e0d,제작2팀,ORGANIZATION,e83f9477-5a8d-4168-ab3b-93508ef9cbb3,jangheon-production,jangheon-fab-2,,,public,
b6cf39a4-6d2d-4de8-9bad-7ecbb54f477e,제작1팀,ORGANIZATION,e83f9477-5a8d-4168-ab3b-93508ef9cbb3,jangheon-production,jangheon-fab-1,,,public,
3dbd0b03-51f5-4d1e-8271-14ff09258dad,철근팀,ORGANIZATION,e83f9477-5a8d-4168-ab3b-93508ef9cbb3,jangheon-production,jangheon-rebar,,,public,
8c74588f-d755-4fd7-b923-26bad7ff0d14,공무팀,ORGANIZATION,e83f9477-5a8d-4168-ab3b-93508ef9cbb3,jangheon-production,jangheon-production-admin,,,public,
e83f9477-5a8d-4168-ab3b-93508ef9cbb3,생산부,ORGANIZATION,c18a8284-0008-48aa-9cdf-9f47ab79a2a9,jangheon,jangheon-production,,,public,
a5b70b22-a7fc-4d01-a7e2-2cc022e808ee,바론컨설턴트,COMPANY,96369f12-6b66-4b2a-a916-d1c99d326f02,baron-group,baroncs,,,public,
4fda8bb7-d6c4-44b7-8da9-e36f2b487732,경영지원부,ORGANIZATION,b2fcf17f-7085-4bfe-9663-d8a2f2f4b2d6,jangheon-sanup,js-mgmt-support,,,public,
6b687038-180a-4141-b8d2-62647068a8ad,안전진단부,ORGANIZATION,369c1843-56af-4344-9c21-0e01197ab861,hanmac,safety-diagnosis,,,public,
36ed0b11-6b5f-4ecd-b5b6-d009d1a8a5f9,건설사업부,ORGANIZATION,d255e6da-4298-4e67-a7cc-45c53d7cdb61,construction-management-h,construction-business,,,public,
d255e6da-4298-4e67-a7cc-45c53d7cdb61,건설사업관리본부,ORGANIZATION,369c1843-56af-4344-9c21-0e01197ab861,hanmac,construction-management-h,,,public,
e5c94be2-a3ce-4044-8b6c-623a45abb428,상하수도부,ORGANIZATION,e76d0596-c4cc-4a1d-b596-f824823a17b4,land-environment-hq,water-sewerage,,,public,
d149e48b-cdd8-4ebc-9880-7108a599b938,수자원부,ORGANIZATION,e76d0596-c4cc-4a1d-b596-f824823a17b4,land-environment-hq,land-env-water-resources,,,public,
40be0d19-b61a-424a-97d6-a4fa14e342f1,도시계획부,ORGANIZATION,e76d0596-c4cc-4a1d-b596-f824823a17b4,land-environment-hq,land-env-urban-planning,,,public,
1c89dcbc-ebe5-41c8-b1a6-71b1dda7db19,환경평가부,ORGANIZATION,e76d0596-c4cc-4a1d-b596-f824823a17b4,land-environment-hq,land-env-assessment,,,public,
e76d0596-c4cc-4a1d-b596-f824823a17b4,국토환경사업본부,ORGANIZATION,369c1843-56af-4344-9c21-0e01197ab861,hanmac,land-environment-hq,,,public,
32f33163-e613-4f21-b285-449afb31346e,지반터널부,ORGANIZATION,c31bcf86-26da-4cd7-8ef3-371f606cbb72,infrastructure-hq,infra-geotech-tunnel,,,public,
1ccb0859-7eda-4f54-8217-3f781ed036ef,구조부,ORGANIZATION,c31bcf86-26da-4cd7-8ef3-371f606cbb72,infrastructure-hq,infra-structures,,,public,
f8ffcb83-b709-48fc-a993-c1caefc1a648,교통부,ORGANIZATION,c31bcf86-26da-4cd7-8ef3-371f606cbb72,infrastructure-hq,traffic,,,public,
cf1818e2-689f-449b-8c99-0ae25953e576,도로부,ORGANIZATION,c31bcf86-26da-4cd7-8ef3-371f606cbb72,infrastructure-hq,infra-road,,,public,
c31bcf86-26da-4cd7-8ef3-371f606cbb72,인프라사업본부,ORGANIZATION,369c1843-56af-4344-9c21-0e01197ab861,hanmac,infrastructure-hq,,,public,
1d784637-6926-4671-975b-3e8e455939a2,안전관리부,ORGANIZATION,369c1843-56af-4344-9c21-0e01197ab861,hanmac,safety-management,,,public,
a2252cac-b321-493a-b177-2e702da0d77d,영업지원,ORGANIZATION,369c1843-56af-4344-9c21-0e01197ab861,hanmac,sales-support,,,public,
445ee8ad-41e3-42fe-902f-e27d937f09c8,인프라 BIM3,ORGANIZATION,d8b41c30-fe0c-4318-9ae3-cae7dcf6c891,infra-solution,infra-bim3,,,public,
52f06c97-9d6f-4819-971b-43303062e193,인프라 BIM2,ORGANIZATION,d8b41c30-fe0c-4318-9ae3-cae7dcf6c891,infra-solution,infra-bim2,,,public,
432b5261-421b-4e5f-914f-32d7d22fd01f,인프라 BIM1,ORGANIZATION,d8b41c30-fe0c-4318-9ae3-cae7dcf6c891,infra-solution,infra-bim1,,,public,
d8b41c30-fe0c-4318-9ae3-cae7dcf6c891,인프라솔루션,ORGANIZATION,56cd0fd7-b62a-43c0-8db9-74a30468d7cb,tdc,infra-solution,,,public,디비전
96f9d9de-e187-4a27-941e-0f57f3b1851a,네이버웍스관리용(바론그룹),ORGANIZATION,96369f12-6b66-4b2a-a916-d1c99d326f02,baron-group,su4,,,private,
d7205737-e7bd-4926-9b76-39f447ff809e,네이버웍스관리용(한맥),ORGANIZATION,369c1843-56af-4344-9c21-0e01197ab861,hanmac,su2,,,private,
a6343ba4-6062-4150-87b7-a9bec4ca34a0,네이버웍스관리용(삼안),ORGANIZATION,9caf62e1-297d-4e8f-870b-61780998bbeb,saman,su1,,,private,
818c856b-9545-442f-b827-d1c569f200b0,기술개발센터(조직도용),ORGANIZATION,9caf62e1-297d-4e8f-870b-61780998bbeb,saman,rnd-center,,,public,
93e1f3dc-503f-40f7-8342-31974b5dd33c,해외사업부,ORGANIZATION,ec325e6f-4333-43ae-a52a-77b742eef7ed,overseas-headquarters,overseas-business,,,public,
ec325e6f-4333-43ae-a52a-77b742eef7ed,해외사업본부,ORGANIZATION,9caf62e1-297d-4e8f-870b-61780998bbeb,saman,overseas-headquarters,,,public,
5b531a28-a222-44de-87f4-8a0560685a25,수력부,ORGANIZATION,f60d0b66-e0c8-46be-88dc-e8bc93b9e149,water-resources-hq,hydropower,,,public,
6c1ed0b6-8141-4ff8-96fb-a84efbe3a2ce,수자원2부,ORGANIZATION,f60d0b66-e0c8-46be-88dc-e8bc93b9e149,water-resources-hq,water-resources-2,,,public,
d59651ea-41d3-45ee-b519-537fc6a97e65,수자원1부,ORGANIZATION,f60d0b66-e0c8-46be-88dc-e8bc93b9e149,water-resources-hq,water-resources-1,,,public,
f60d0b66-e0c8-46be-88dc-e8bc93b9e149,수자원본부,ORGANIZATION,9caf62e1-297d-4e8f-870b-61780998bbeb,saman,water-resources-hq,,,public,
1596d8d3-e92d-4c95-8e3e-56c824991ee9,물환경3부,ORGANIZATION,9b9b8b70-b058-4f22-a0c3-e63bc0c7e1b8,water-environment-hq,water-environment-3,,,public,
9ff11813-07ae-41d2-b2af-ac4038dafde2,물환경2부,ORGANIZATION,9b9b8b70-b058-4f22-a0c3-e63bc0c7e1b8,water-environment-hq,water-environment-2,,,public,
2e4d1c4f-f859-4bdc-8531-8c2f29d74cc0,물환경1부,ORGANIZATION,9b9b8b70-b058-4f22-a0c3-e63bc0c7e1b8,water-environment-hq,water-environment-1,,,public,
9b9b8b70-b058-4f22-a0c3-e63bc0c7e1b8,물환경본부,ORGANIZATION,9caf62e1-297d-4e8f-870b-61780998bbeb,saman,water-environment-hq,,,public,
25e9f426-8012-43bd-8d2f-35ad76f0236a,환경평가부,ORGANIZATION,7ee20c20-01a7-4e39-9a58-a2ffdad153dd,railway-headquarters,environment-assessment,,,public,
a4722e17-c3f9-49d9-8535-4c92d03ccaae,철도2부,ORGANIZATION,7ee20c20-01a7-4e39-9a58-a2ffdad153dd,railway-headquarters,railway-2,,,public,
c826ecf7-c2f9-49a1-ab72-0413216999af,철도1부,ORGANIZATION,7ee20c20-01a7-4e39-9a58-a2ffdad153dd,railway-headquarters,railway-1,,,public,
7ee20c20-01a7-4e39-9a58-a2ffdad153dd,철도본부,ORGANIZATION,9caf62e1-297d-4e8f-870b-61780998bbeb,saman,railway-headquarters,,,public,
a1bca214-1f8a-403d-8f0b-c6c3aa71bb5a,안전진단팀,ORGANIZATION,967f657c-04e2-49c1-9cb9-f6afba27c0fa,structures,safety-inspection,,,public,
967f657c-04e2-49c1-9cb9-f6afba27c0fa,구조부,ORGANIZATION,dfdc84b7-87a1-4383-8df7-5e656193e79c,road-headquarters,structures,,,public,
c172de4d-1e61-47d5-9446-7bb36ea50063,교통계획부,ORGANIZATION,dfdc84b7-87a1-4383-8df7-5e656193e79c,road-headquarters,transport-planning,,,public,
6760a731-d172-4bc4-8ed4-1b625752c4a2,지반터널부,ORGANIZATION,dfdc84b7-87a1-4383-8df7-5e656193e79c,road-headquarters,geotech-tunnel,,,public,
0b07f990-3ddb-45fa-b3d1-6233e3580f71,도로부,ORGANIZATION,dfdc84b7-87a1-4383-8df7-5e656193e79c,road-headquarters,road,,,public,
dfdc84b7-87a1-4383-8df7-5e656193e79c,도로본부,ORGANIZATION,9caf62e1-297d-4e8f-870b-61780998bbeb,saman,road-headquarters,,,public,
953a07f6-5ff9-46ed-9a16-bd3066ccec12,조경레저부,ORGANIZATION,95d10fc3-b5d1-4613-9b0a-f9e6cae90d83,land-development,landscape-leisure,,,public,
ed9290e8-7296-47fc-b72f-6ccc45765d4e,도시개발부,ORGANIZATION,95d10fc3-b5d1-4613-9b0a-f9e6cae90d83,land-development,urban-development,,,public,
02ab05a1-caa8-41c9-8c9a-91de614724fd,도시계획부,ORGANIZATION,95d10fc3-b5d1-4613-9b0a-f9e6cae90d83,land-development,urban-planning,,,public,
95d10fc3-b5d1-4613-9b0a-f9e6cae90d83,국토개발본부,ORGANIZATION,9caf62e1-297d-4e8f-870b-61780998bbeb,saman,land-development,,,public,
f36e2211-8cfd-4813-8618-34e606fe73ac,항만부,ORGANIZATION,cd10e627-808b-4c98-8fdd-d47c84ce57d6,plant-headquarters,harbor,,,public,
fec713bf-ac20-4480-bd74-686a2e6d92b3,플랜트2부,ORGANIZATION,cd10e627-808b-4c98-8fdd-d47c84ce57d6,plant-headquarters,plant-2,,,public,
ef3adb8e-3405-4027-a0f8-5d3d5bf11e84,플랜트1부,ORGANIZATION,cd10e627-808b-4c98-8fdd-d47c84ce57d6,plant-headquarters,plant-1,,,public,
cd10e627-808b-4c98-8fdd-d47c84ce57d6,플랜트본부,ORGANIZATION,9caf62e1-297d-4e8f-870b-61780998bbeb,saman,plant-headquarters,,,public,
99735f38-47d6-490b-ad54-8090b3bef91f,호남지역총괄본부,ORGANIZATION,8bbd9aad-1b53-4504-befb-e8ae8792340f,cm-division,honam-headquarters,,,public,
8bbd9aad-1b53-4504-befb-e8ae8792340f,CM사업부,ORGANIZATION,92b4fd3c-91ac-41d2-8757-9344861b97aa,cm-headquarters,cm-division,,,public,
92b4fd3c-91ac-41d2-8757-9344861b97aa,CM본부,ORGANIZATION,9caf62e1-297d-4e8f-870b-61780998bbeb,saman,cm-headquarters,,,public,
fe58cad4-1fa6-4b87-a2eb-51b9ac41320e,사업개발실,ORGANIZATION,9caf62e1-297d-4e8f-870b-61780998bbeb,saman,business-development,,,public,
338fb8af-b594-4c41-9984-54ea4b37637b,안전품질관리실,ORGANIZATION,9caf62e1-297d-4e8f-870b-61780998bbeb,saman,safety-quality,,,public,
1edc196d-020c-4519-9ec4-3d23b99076e6,자산경영실,ORGANIZATION,9caf62e1-297d-4e8f-870b-61780998bbeb,saman,asset-management,,,public,
7adb550b-1756-49f6-b6cc-55b7b426ed52,인사총무부,ORGANIZATION,9bf67270-e15e-4278-b407-02dec5672876,business-strategy,hr-admin,,,public,
01fcbee1-df33-4ee9-bf2b-6d9eb81917d9,대외협력팀,ORGANIZATION,a16f49c4-6828-4fde-a164-43099c4560c4,planning,external-relations,,,public,
cdc40c0b-f985-461a-be18-f8c8e82f31e8,재무회계팀,ORGANIZATION,a16f49c4-6828-4fde-a164-43099c4560c4,planning,finance,,,public,
c6aa2133-ded0-451c-b51b-27faa8b56507,PQ팀,ORGANIZATION,a16f49c4-6828-4fde-a164-43099c4560c4,planning,pq-team,,,public,
ca54cffe-ad30-4f9e-983a-88a85c70404d,업무팀,ORGANIZATION,d656c134-a50b-43b9-8c2d-fb3738dd0f9f,halla-mgmt-support-hq,halla-operations,,,public,
a16f49c4-6828-4fde-a164-43099c4560c4,기획부,ORGANIZATION,9bf67270-e15e-4278-b407-02dec5672876,business-strategy,planning,,,public,
9bf67270-e15e-4278-b407-02dec5672876,경영전략본부,ORGANIZATION,9caf62e1-297d-4e8f-870b-61780998bbeb,saman,business-strategy,,,public,
896da8ab-50b7-4a63-abbc-c85037b63acc,시공BIM,ORGANIZATION,56cd0fd7-b62a-43c0-8db9-74a30468d7cb,tdc,construction-bim,,,public,
410a25b1-cd84-46d4-b4d6-8627b397ca42,스마트건설,ORGANIZATION,56cd0fd7-b62a-43c0-8db9-74a30468d7cb,tdc,smart-construction,,,public,
68c85ffe-5942-42e7-9785-b2c29b18ecb9,수자원,ORGANIZATION,56cd0fd7-b62a-43c0-8db9-74a30468d7cb,tdc,water-resources,,,public,
27683cb5-98ac-49cf-ac57-8157d7d2a663,PM,ORGANIZATION,9e5919f5-0839-4d98-b2e8-dee2ef518b1d,gsim-dev,project-management,,,public,
39ad464a-8468-4acb-8f05-29bc828a1576,GSIM,ORGANIZATION,9e5919f5-0839-4d98-b2e8-dee2ef518b1d,gsim-dev,gsim,,,public,
2501e4a7-bf41-48df-afae-76676e0169f1,bCMf,ORGANIZATION,9e5919f5-0839-4d98-b2e8-dee2ef518b1d,gsim-dev,bcmf,,,public,
9e5919f5-0839-4d98-b2e8-dee2ef518b1d,GSIM개발,ORGANIZATION,56cd0fd7-b62a-43c0-8db9-74a30468d7cb,tdc,gsim-dev,,,public,
b83c124a-8d5b-4b14-9347-9d1dca2b42fa,웹디자인,ORGANIZATION,f8366e19-d767-46db-a789-49e36d88a8fc,web-solutions,web-design,,,public,
2403109e-1e43-4390-8d2a-6f0566be028e,ERP,ORGANIZATION,f8366e19-d767-46db-a789-49e36d88a8fc,web-solutions,erp,,,public,
54986461-8cf1-4791-b130-8fecbff5f5a2,솔루션개발,ORGANIZATION,f8366e19-d767-46db-a789-49e36d88a8fc,web-solutions,solution-dev,,,public,
f8366e19-d767-46db-a789-49e36d88a8fc,웹솔루션,ORGANIZATION,56cd0fd7-b62a-43c0-8db9-74a30468d7cb,tdc,web-solutions,,,public,
ec003372-962a-4d7b-b90a-edf9dfbb6eea,Abut&시공통합관제,ORGANIZATION,d394dcf2-d474-4059-9cbb-0fca343ec38c,graphics,abut-control,,,public,
52266543-a90b-4441-99c6-51f454b6059a,EG-BIM Draw,ORGANIZATION,d394dcf2-d474-4059-9cbb-0fca343ec38c,graphics,eg-bim-draw,,,public,
1d74bebb-c5a1-49d4-bec4-90f0c89ad21f,HmEG,ORGANIZATION,d394dcf2-d474-4059-9cbb-0fca343ec38c,graphics,hmeg,,,public,
87831866-07de-4e3c-b158-28188a3c1edb,Modeler,ORGANIZATION,d394dcf2-d474-4059-9cbb-0fca343ec38c,graphics,modeler,,,public,
d394dcf2-d474-4059-9cbb-0fca343ec38c,그래픽스,ORGANIZATION,56cd0fd7-b62a-43c0-8db9-74a30468d7cb,tdc,graphics,,,public,
d43eed1b-3a36-4624-a040-7f2827016df6,Strana,ORGANIZATION,56cd0fd7-b62a-43c0-8db9-74a30468d7cb,tdc,strana,,,public,
78f251f6-d35b-422d-92ab-7fabd80bef85,구조물S/W,ORGANIZATION,56cd0fd7-b62a-43c0-8db9-74a30468d7cb,tdc,structural-software,,,public,
b06b8821-b74c-44d5-89c4-3a94e9979161,Watch BIM,ORGANIZATION,9b211775-e6b6-4caf-8c98-91249186e3d9,infra-solution-dev,watch-bim,,,public,
79923c99-1aab-4950-b69e-6312636f544f,Primal 평면,ORGANIZATION,9b211775-e6b6-4caf-8c98-91249186e3d9,infra-solution-dev,primal-plan,,,public,
28fba7fd-6af5-43c1-807b-fb97d6eafada,Way Draw,ORGANIZATION,9b211775-e6b6-4caf-8c98-91249186e3d9,infra-solution-dev,way-draw,,,public,
0cec58e8-efbe-4f93-a734-d1b9cc7747b7,비탈면/구조물,ORGANIZATION,9b211775-e6b6-4caf-8c98-91249186e3d9,infra-solution-dev,slope-structures,,,public,
9b211775-e6b6-4caf-8c98-91249186e3d9,인프라솔루션 개발,ORGANIZATION,56cd0fd7-b62a-43c0-8db9-74a30468d7cb,tdc,infra-solution-dev,,,public,
f39ef0c8-0ad0-49cd-97ff-b0672591cfe3,단지설계 개발,ORGANIZATION,d015f4fc-6ce1-4258-be37-6c531cf75c6c,cheonjijin,site-design-dev,,,public,
35cc1fdf-6c0e-4b0e-8ce8-1adc918b8cbf,용지도셀,ORGANIZATION,d015f4fc-6ce1-4258-be37-6c531cf75c6c,cheonjijin,land-map-cell,,,public,
c3e59dbc-a315-47f9-8787-2a792a311e32,천지인셀,ORGANIZATION,d015f4fc-6ce1-4258-be37-6c531cf75c6c,cheonjijin,cheonjijin-cell,,,public,
d015f4fc-6ce1-4258-be37-6c531cf75c6c,천지인,ORGANIZATION,56cd0fd7-b62a-43c0-8db9-74a30468d7cb,tdc,cheonjijin,,,public,
61e9ed21-e100-475a-a1e6-bdb2a302db95,상하수도,ORGANIZATION,56cd0fd7-b62a-43c0-8db9-74a30468d7cb,tdc,water-sewer,,,public,
0e206c1f-8e9b-43c2-91ce-a17bf62854c8,단가산출,ORGANIZATION,0471c240-7080-4648-86a6-5fdecff9e148,cost-control,cost-estimate,,,public,
bc903928-61cb-45a8-9b4a-794f62a9f8a6,공정관리,ORGANIZATION,0471c240-7080-4648-86a6-5fdecff9e148,cost-control,schedule-control,,,public,
0471c240-7080-4648-86a6-5fdecff9e148,CC,ORGANIZATION,56cd0fd7-b62a-43c0-8db9-74a30468d7cb,tdc,cost-control,,,public,
fcb4c9d3-ea2c-48c6-92cc-63706f53fa21,터널,ORGANIZATION,582972ce-6b06-48c8-aa30-31a5285e160e,structural-division,tunnel,,,public,
fab2c4ca-4081-4f64-924d-f32bdf2e61b4,CM기획,ORGANIZATION,582972ce-6b06-48c8-aa30-31a5285e160e,structural-division,cm-planning,,,public,
1e1a999a-38f0-40f1-ae89-62e6031a29e0,하부구조,ORGANIZATION,582972ce-6b06-48c8-aa30-31a5285e160e,structural-division,substructure,,,public,
9eb73493-8aa1-43d4-99a8-424b1a7d60be,구조물계획,ORGANIZATION,582972ce-6b06-48c8-aa30-31a5285e160e,structural-division,structure-planning,,,public,
3d147a08-00b9-47c7-940a-d75c36a6ce81,일반구조물,ORGANIZATION,582972ce-6b06-48c8-aa30-31a5285e160e,structural-division,structural-design,,,public,
1d39617e-8e50-4081-bcd5-551e7842f7b3,DfMA,ORGANIZATION,582972ce-6b06-48c8-aa30-31a5285e160e,structural-division,dfma,,,public,
582972ce-6b06-48c8-aa30-31a5285e160e,일반구조물 div,ORGANIZATION,56cd0fd7-b62a-43c0-8db9-74a30468d7cb,tdc,structural-division,,,public,
56cd0fd7-b62a-43c0-8db9-74a30468d7cb,기술개발센터,ORGANIZATION,5530ca6e-c5e6-4bf0-84d6-76c6a8fb70ee,gpdtdc,tdc,,,internal,
59b83b98-b604-4621-8527-b872be47accc,네이버웍스관리용(총센),ORGANIZATION,5530ca6e-c5e6-4bf0-84d6-76c6a8fb70ee,gpdtdc,su3,,,private,
586434c5-3460-458b-b2e5-488b0e77fa21,솔루션통합,ORGANIZATION,761a8725-9c19-442c-986c-0319e33a5b1e,gpd,solution-integration,,,public,
df6a504d-75b9-4a21-a67e-1522cc18111c,협업증진,ORGANIZATION,761a8725-9c19-442c-986c-0319e33a5b1e,gpd,collaboration,,,public,
6f420ad4-2345-43b5-9c2d-7ba0dae8cae4,디자인기획,ORGANIZATION,761a8725-9c19-442c-986c-0319e33a5b1e,gpd,design-planning,,,public,
88d1d1ee-795a-4b67-a8e7-bb0d4d7ac49f,ERP기획,ORGANIZATION,761a8725-9c19-442c-986c-0319e33a5b1e,gpd,erp-planning,,,public,
c6b1266c-564b-4543-baba-d78807a3d1b4,경영기획,ORGANIZATION,761a8725-9c19-442c-986c-0319e33a5b1e,gpd,management-planning,,,public,
3a660456-eceb-472b-a9a9-f2a5b0ce972b,기술기획,ORGANIZATION,761a8725-9c19-442c-986c-0319e33a5b1e,gpd,tech-planning,,,public,
556798a6-b45e-4822-b6f1-42046b6d0001,전산관리TF,ORGANIZATION,761a8725-9c19-442c-986c-0319e33a5b1e,gpd,it-admin-tf,,,internal,
539f598d-e6f1-4fe2-be48-466448d8d803,인재성장,ORGANIZATION,761a8725-9c19-442c-986c-0319e33a5b1e,gpd,talent-growth,,,public,
761a8725-9c19-442c-986c-0319e33a5b1e,총괄기획실,ORGANIZATION,5530ca6e-c5e6-4bf0-84d6-76c6a8fb70ee,gpdtdc,gpd,,,public,
e57cb22c-383e-4489-8c2f-0c5431917e86,PTC,COMPANY,96369f12-6b66-4b2a-a916-d1c99d326f02,baron-group,ptc,,,public,
9607eb7b-04d2-42ab-80fe-780fe21c7e8f,Personal,PERSONAL,,,personal,개인 사용자 기본 루트 테넌트,,public,
5a03efd2-e62f-4243-800d-58334bf48b2f,한라산업개발,ORGANIZATION,96369f12-6b66-4b2a-a916-d1c99d326f02,baron-group,halla,,,public,
b2fcf17f-7085-4bfe-9663-d8a2f2f4b2d6,장헌산업,ORGANIZATION,96369f12-6b66-4b2a-a916-d1c99d326f02,baron-group,jangheon-sanup,,,public,
c18a8284-0008-48aa-9cdf-9f47ab79a2a9,(주)장헌,ORGANIZATION,96369f12-6b66-4b2a-a916-d1c99d326f02,baron-group,jangheon,,,public,
96369f12-6b66-4b2a-a916-d1c99d326f02,바론그룹,COMPANY_GROUP,038326b6-954a-48a7-a85f-efd83f62b82a,hanmac-family,baron-group,네이버웍스 바론그룹 BARONGROUP_DOMAIN_ID,,public,
5530ca6e-c5e6-4bf0-84d6-76c6a8fb70ee,총괄기획&기술개발센터,COMPANY,038326b6-954a-48a7-a85f-efd83f62b82a,hanmac-family,gpdtdc,네이버웍스 총괄기획&기술개발센터 GPDTDC_DOMAIN_ID,baroncs.co.kr,public,
369c1843-56af-4344-9c21-0e01197ab861,한맥기술,COMPANY,038326b6-954a-48a7-a85f-efd83f62b82a,hanmac-family,hanmac,네이버웍스 한맥 HANMAC_DOMAIN_ID,hanmaceng.co.kr,public,
9caf62e1-297d-4e8f-870b-61780998bbeb,삼안,COMPANY,038326b6-954a-48a7-a85f-efd83f62b82a,hanmac-family,saman,네이버웍스 삼안 SAMAN_DOMAIN_ID,samaneng.com,public,
038326b6-954a-48a7-a85f-efd83f62b82a,한맥가족,COMPANY_GROUP,,,hanmac-family,한맥가족 기본 루트 테넌트,,public,
1 tenant_id name type parent_tenant_id parent_tenant_slug slug memo email_domain visibility org_unit_type
2 cd1ebc22-4b5e-4242-bb87-eb88db32286c 업무 ORGANIZATION a16f49c4-6828-4fde-a164-43099c4560c4 planning operations public
3 0e13d342-d3cf-46b5-8096-4e7883b79b01 서산시자원회수시설 ORGANIZATION d7379c32-0b79-482e-9c4d-d83ad425c3fc halla-operation-sites ops-seosan-recovery public
4 41118f16-7f5c-4209-bd83-183822bc00ed 안성제4차산업단지폐수처리 ORGANIZATION d7379c32-0b79-482e-9c4d-d83ad425c3fc halla-operation-sites ops-anseong-wwtp public
5 ad6f20e9-7928-4322-932c-7c3cb2a313cb 온산바이오 ORGANIZATION d7379c32-0b79-482e-9c4d-d83ad425c3fc halla-operation-sites ops-onsan-bio public
6 03d8cf87-4b40-4784-a6cf-fcc11371f40f 울산민자소각 ORGANIZATION d7379c32-0b79-482e-9c4d-d83ad425c3fc halla-operation-sites ops-ulsan-incineration public
7 d7379c32-0b79-482e-9c4d-d83ad425c3fc 운영사업소 ORGANIZATION 5a03efd2-e62f-4243-800d-58334bf48b2f halla halla-operation-sites public
8 551991d8-1f74-4ad0-a0c5-bc5a11968398 부산항 신항 ORGANIZATION 99199302-f04f-47ad-9f9f-2afe2db9826a halla-construction-sites site-busan-new-port public
9 e77b4bf1-a126-4b4e-a18a-8d905e958873 수도권광역급행철도B 제4공구 ORGANIZATION 99199302-f04f-47ad-9f9f-2afe2db9826a halla-construction-sites site-gtx-b-4 public
10 3b5151f6-1a01-484a-bfb7-2e60d2aa0b49 경산시 국도대체 ORGANIZATION 99199302-f04f-47ad-9f9f-2afe2db9826a halla-construction-sites site-gyeongsan-road public
11 44c6e400-daf0-42a2-90df-945921788f99 인덕원 동탄 복선전철 제7공구 ORGANIZATION 99199302-f04f-47ad-9f9f-2afe2db9826a halla-construction-sites site-indeokwon-dongtan-7 public
12 2bc22118-9a70-4d5b-8a3f-cf65432a8bbb 인덕원 동탄 복선전철 제3공구 ORGANIZATION 99199302-f04f-47ad-9f9f-2afe2db9826a halla-construction-sites site-indeokwon-dongtan-3 public
13 1134fa6a-9b0b-4702-a1e7-39948c8c451a 제주공공하수처리 ORGANIZATION 99199302-f04f-47ad-9f9f-2afe2db9826a halla-construction-sites site-jeju-sewage public
14 36aec47e-90fc-42cb-8229-3e20423d0424 성남시생활폐기물처리 ORGANIZATION 99199302-f04f-47ad-9f9f-2afe2db9826a halla-construction-sites site-seongnam-waste public
15 5b0b806c-f189-46ea-8771-ebdafcf45afa 광탄공공하수처리 ORGANIZATION 99199302-f04f-47ad-9f9f-2afe2db9826a halla-construction-sites site-gwangtan-sewage public
16 d2323d9a-c959-48c0-831b-4bb71e48b2e5 인천국제공항 화물 ORGANIZATION 99199302-f04f-47ad-9f9f-2afe2db9826a halla-construction-sites site-incheon-air-cargo public
17 32a83ce1-03f1-4daa-b60f-8e64fad83ac6 수도권매립지 제2매립장 ORGANIZATION 99199302-f04f-47ad-9f9f-2afe2db9826a halla-construction-sites site-sudokwon-landfill-2 public
18 e39ba0af-c3e9-429b-91ec-0a3453a5692e 온산하수처리 ORGANIZATION 99199302-f04f-47ad-9f9f-2afe2db9826a halla-construction-sites site-onsan-sewage public
19 25f51047-3108-4ff3-98f4-b7f5bce334c5 신천공공하수처리 ORGANIZATION 99199302-f04f-47ad-9f9f-2afe2db9826a halla-construction-sites site-sincheon-sewage public
20 f0ae9e81-65a5-4bab-a98d-79349bbaa501 장량공공하수처리 ORGANIZATION 99199302-f04f-47ad-9f9f-2afe2db9826a halla-construction-sites site-jangnyang-sewage public
21 b662cfdb-aae3-48b7-b1d3-2ef050dce027 아포공공하수처리 ORGANIZATION 99199302-f04f-47ad-9f9f-2afe2db9826a halla-construction-sites site-apo-sewage public
22 76808046-cd35-4813-b240-c323291fa2d8 광주공공폐수처리 ORGANIZATION 99199302-f04f-47ad-9f9f-2afe2db9826a halla-construction-sites site-gwangju-wastewater public
23 bda54a49-8282-4f91-9773-645e6a1f2a3b 도척 실촌간 도로 ORGANIZATION 99199302-f04f-47ad-9f9f-2afe2db9826a halla-construction-sites site-docheok-silchon-road public
24 02a9e89b-a0a0-4202-adde-870194c35351 여주부평천 ORGANIZATION 99199302-f04f-47ad-9f9f-2afe2db9826a halla-construction-sites site-yeoju-bupyeongcheon public
25 9b1fb915-f50b-49b9-a9f9-a9089a825b1f 옥정 공공하수처리 ORGANIZATION 99199302-f04f-47ad-9f9f-2afe2db9826a halla-construction-sites site-okjeong-sewage public
26 29d9fa54-6d8d-49f5-98ca-2c720aced55e 부천시 굴포천 ORGANIZATION 99199302-f04f-47ad-9f9f-2afe2db9826a halla-construction-sites site-bucheon-gulpocheon public
27 99199302-f04f-47ad-9f9f-2afe2db9826a 시공현장 ORGANIZATION 5a03efd2-e62f-4243-800d-58334bf48b2f halla halla-construction-sites public
28 2dbbdbf8-26b2-461e-bbd7-1fe09b4e36ee 안전관리팀 ORGANIZATION 4b81d408-d81c-43c7-9f87-b6c806db4d7b halla-safety-hq halla-safety-team public
29 4b81d408-d81c-43c7-9f87-b6c806db4d7b 안전관리본부 ORGANIZATION 5a03efd2-e62f-4243-800d-58334bf48b2f halla halla-safety-hq public
30 69aa9667-4997-41f8-9898-e470cfc778e5 기술영업팀 ORGANIZATION 1512e429-fb95-4c0d-9409-f0a3286061f2 halla-tech-sales-hq halla-tech-sales-team public
31 1512e429-fb95-4c0d-9409-f0a3286061f2 기술영업본부 ORGANIZATION 5a03efd2-e62f-4243-800d-58334bf48b2f halla halla-tech-sales-hq public
32 6f9e45f7-63fb-464e-b47c-915fa25f782f 설계팀 ORGANIZATION 2ea01ea1-6d09-4997-ba67-73cbae7aa7dd halla-env-plant-hq halla-env-plant-design public
33 69d6d246-b281-4da7-be83-fede8e3dc5bd 사업관리팀 ORGANIZATION 2ea01ea1-6d09-4997-ba67-73cbae7aa7dd halla-env-plant-hq halla-env-project-mgmt public
34 2ea01ea1-6d09-4997-ba67-73cbae7aa7dd 환경플랜트사업본부 ORGANIZATION 5a03efd2-e62f-4243-800d-58334bf48b2f halla halla-env-plant-hq public
35 45519f6d-ba67-42e2-9b54-f80f1a950a8c 사업관리팀 ORGANIZATION 1d5da961-7f32-4032-a86c-26e8edbcb8ee halla-infra-business-hq halla-infra-project-mgmt public
36 1d5da961-7f32-4032-a86c-26e8edbcb8ee 기반사업본부 ORGANIZATION 5a03efd2-e62f-4243-800d-58334bf48b2f halla halla-infra-business-hq public
37 03af0690-af91-468a-9892-0152c7309a4b 운영사업실 ORGANIZATION d656c134-a50b-43b9-8c2d-fb3738dd0f9f halla-mgmt-support-hq halla-operations-office public
38 6e9b627f-5304-4e7d-99fc-77fc2328d004 경영지원팀 ORGANIZATION d656c134-a50b-43b9-8c2d-fb3738dd0f9f halla-mgmt-support-hq halla-mgmt-support public
39 43c0fb29-84dd-49d2-a2b8-33b6659f4607 사업지원팀 ORGANIZATION d656c134-a50b-43b9-8c2d-fb3738dd0f9f halla-mgmt-support-hq halla-business-support public
40 d656c134-a50b-43b9-8c2d-fb3738dd0f9f 경영지원본부 ORGANIZATION 5a03efd2-e62f-4243-800d-58334bf48b2f halla halla-mgmt-support-hq public
41 940cc09c-32f5-4a02-8213-fb02521189d0 영업총괄 ORGANIZATION 5a03efd2-e62f-4243-800d-58334bf48b2f halla halla-general-sales public
42 57496cae-a081-4836-a20e-75c78b62257f 업무총괄 ORGANIZATION 5a03efd2-e62f-4243-800d-58334bf48b2f halla halla-general-business public
43 81e94e6c-e27a-4e36-b0f9-bf8823c96493 임원실 ORGANIZATION 5a03efd2-e62f-4243-800d-58334bf48b2f halla halla-executive public
44 786dd00c-b0c1-4db9-b25b-1afecd6a7a41 안전관리 ORGANIZATION 94c23f79-a213-4a9e-9c5d-7751777f2fe8 js-construction-hq js-safety-management public
45 5fbf6f2c-6b12-4124-a457-d1064dbb8677 현장 ORGANIZATION 94c23f79-a213-4a9e-9c5d-7751777f2fe8 js-construction-hq js-site public
46 dd82bb7b-43d8-4744-ab65-9b47ea492ac4 공무 ORGANIZATION 94c23f79-a213-4a9e-9c5d-7751777f2fe8 js-construction-hq js-construction-admin public
47 94c23f79-a213-4a9e-9c5d-7751777f2fe8 건설본부 ORGANIZATION b2fcf17f-7085-4bfe-9663-d8a2f2f4b2d6 jangheon-sanup js-construction-hq public
48 4738ed53-51cc-4dcf-9885-44c9feb26760 견적 ORGANIZATION c6ff3238-f001-4cdd-b00c-0d137a3baff8 js-tech-sales-hq js-estimation public
49 063a7d31-4aa4-4904-9f32-9b092116145e 기술지원 ORGANIZATION c6ff3238-f001-4cdd-b00c-0d137a3baff8 js-tech-sales-hq js-tech-support public
50 91063309-efb3-48b7-b55b-a6e79c9eb202 영업 ORGANIZATION c6ff3238-f001-4cdd-b00c-0d137a3baff8 js-tech-sales-hq js-sales public
51 c6ff3238-f001-4cdd-b00c-0d137a3baff8 기술영업본부 ORGANIZATION b2fcf17f-7085-4bfe-9663-d8a2f2f4b2d6 jangheon-sanup js-tech-sales-hq public
52 64973a3e-102e-4efd-8147-5be720b89c36 임원실 ORGANIZATION b2fcf17f-7085-4bfe-9663-d8a2f2f4b2d6 jangheon-sanup jangheon-sanup-executive public
53 6f6f64d3-a555-4680-9b37-a276688b2dfa 설계팀 ORGANIZATION e57cb22c-383e-4489-8c2f-0c5431917e86 ptc ptc-design public
54 f617365d-2d1a-4d1a-a743-82b3599c8946 시공팀 ORGANIZATION e57cb22c-383e-4489-8c2f-0c5431917e86 ptc ptc-construction public
55 e629fa7d-c945-4952-b79f-1e23ecf9e7cb 사업관리팀 ORGANIZATION e57cb22c-383e-4489-8c2f-0c5431917e86 ptc ptc-project-management public
56 f19677d0-0e91-4da7-a4a4-57f3b2815154 영업팀 ORGANIZATION e57cb22c-383e-4489-8c2f-0c5431917e86 ptc ptc-sales public
57 94be067d-f369-4f6b-a6ec-2d063694c929 임원실 ORGANIZATION e57cb22c-383e-4489-8c2f-0c5431917e86 ptc ptc-executive public
58 07ef09e2-530b-498b-9f25-610500d4acb3 업무지원팀 ORGANIZATION c18a8284-0008-48aa-9cdf-9f47ab79a2a9 jangheon jangheon-business-support public
59 50b42506-f10c-4cb8-af7b-5b7c6aa63276 품질팀 ORGANIZATION e83f9477-5a8d-4168-ab3b-93508ef9cbb3 jangheon-production jangheon-quality public
60 f01cc7b9-aaa0-40c1-9124-9e81c28a6e0d 제작2팀 ORGANIZATION e83f9477-5a8d-4168-ab3b-93508ef9cbb3 jangheon-production jangheon-fab-2 public
61 b6cf39a4-6d2d-4de8-9bad-7ecbb54f477e 제작1팀 ORGANIZATION e83f9477-5a8d-4168-ab3b-93508ef9cbb3 jangheon-production jangheon-fab-1 public
62 3dbd0b03-51f5-4d1e-8271-14ff09258dad 철근팀 ORGANIZATION e83f9477-5a8d-4168-ab3b-93508ef9cbb3 jangheon-production jangheon-rebar public
63 8c74588f-d755-4fd7-b923-26bad7ff0d14 공무팀 ORGANIZATION e83f9477-5a8d-4168-ab3b-93508ef9cbb3 jangheon-production jangheon-production-admin public
64 e83f9477-5a8d-4168-ab3b-93508ef9cbb3 생산부 ORGANIZATION c18a8284-0008-48aa-9cdf-9f47ab79a2a9 jangheon jangheon-production public
65 a5b70b22-a7fc-4d01-a7e2-2cc022e808ee 바론컨설턴트 COMPANY 96369f12-6b66-4b2a-a916-d1c99d326f02 baron-group baroncs public
66 4fda8bb7-d6c4-44b7-8da9-e36f2b487732 경영지원부 ORGANIZATION b2fcf17f-7085-4bfe-9663-d8a2f2f4b2d6 jangheon-sanup js-mgmt-support public
67 6b687038-180a-4141-b8d2-62647068a8ad 안전진단부 ORGANIZATION 369c1843-56af-4344-9c21-0e01197ab861 hanmac safety-diagnosis public
68 36ed0b11-6b5f-4ecd-b5b6-d009d1a8a5f9 건설사업부 ORGANIZATION d255e6da-4298-4e67-a7cc-45c53d7cdb61 construction-management-h construction-business public
69 d255e6da-4298-4e67-a7cc-45c53d7cdb61 건설사업관리본부 ORGANIZATION 369c1843-56af-4344-9c21-0e01197ab861 hanmac construction-management-h public
70 e5c94be2-a3ce-4044-8b6c-623a45abb428 상하수도부 ORGANIZATION e76d0596-c4cc-4a1d-b596-f824823a17b4 land-environment-hq water-sewerage public
71 d149e48b-cdd8-4ebc-9880-7108a599b938 수자원부 ORGANIZATION e76d0596-c4cc-4a1d-b596-f824823a17b4 land-environment-hq land-env-water-resources public
72 40be0d19-b61a-424a-97d6-a4fa14e342f1 도시계획부 ORGANIZATION e76d0596-c4cc-4a1d-b596-f824823a17b4 land-environment-hq land-env-urban-planning public
73 1c89dcbc-ebe5-41c8-b1a6-71b1dda7db19 환경평가부 ORGANIZATION e76d0596-c4cc-4a1d-b596-f824823a17b4 land-environment-hq land-env-assessment public
74 e76d0596-c4cc-4a1d-b596-f824823a17b4 국토환경사업본부 ORGANIZATION 369c1843-56af-4344-9c21-0e01197ab861 hanmac land-environment-hq public
75 32f33163-e613-4f21-b285-449afb31346e 지반터널부 ORGANIZATION c31bcf86-26da-4cd7-8ef3-371f606cbb72 infrastructure-hq infra-geotech-tunnel public
76 1ccb0859-7eda-4f54-8217-3f781ed036ef 구조부 ORGANIZATION c31bcf86-26da-4cd7-8ef3-371f606cbb72 infrastructure-hq infra-structures public
77 f8ffcb83-b709-48fc-a993-c1caefc1a648 교통부 ORGANIZATION c31bcf86-26da-4cd7-8ef3-371f606cbb72 infrastructure-hq traffic public
78 cf1818e2-689f-449b-8c99-0ae25953e576 도로부 ORGANIZATION c31bcf86-26da-4cd7-8ef3-371f606cbb72 infrastructure-hq infra-road public
79 c31bcf86-26da-4cd7-8ef3-371f606cbb72 인프라사업본부 ORGANIZATION 369c1843-56af-4344-9c21-0e01197ab861 hanmac infrastructure-hq public
80 1d784637-6926-4671-975b-3e8e455939a2 안전관리부 ORGANIZATION 369c1843-56af-4344-9c21-0e01197ab861 hanmac safety-management public
81 a2252cac-b321-493a-b177-2e702da0d77d 영업지원 ORGANIZATION 369c1843-56af-4344-9c21-0e01197ab861 hanmac sales-support public
82 445ee8ad-41e3-42fe-902f-e27d937f09c8 인프라 BIM3 ORGANIZATION d8b41c30-fe0c-4318-9ae3-cae7dcf6c891 infra-solution infra-bim3 public
83 52f06c97-9d6f-4819-971b-43303062e193 인프라 BIM2 ORGANIZATION d8b41c30-fe0c-4318-9ae3-cae7dcf6c891 infra-solution infra-bim2 public
84 432b5261-421b-4e5f-914f-32d7d22fd01f 인프라 BIM1 ORGANIZATION d8b41c30-fe0c-4318-9ae3-cae7dcf6c891 infra-solution infra-bim1 public
85 d8b41c30-fe0c-4318-9ae3-cae7dcf6c891 인프라솔루션 ORGANIZATION 56cd0fd7-b62a-43c0-8db9-74a30468d7cb tdc infra-solution public 디비전
86 96f9d9de-e187-4a27-941e-0f57f3b1851a 네이버웍스관리용(바론그룹) ORGANIZATION 96369f12-6b66-4b2a-a916-d1c99d326f02 baron-group su4 private
87 d7205737-e7bd-4926-9b76-39f447ff809e 네이버웍스관리용(한맥) ORGANIZATION 369c1843-56af-4344-9c21-0e01197ab861 hanmac su2 private
88 a6343ba4-6062-4150-87b7-a9bec4ca34a0 네이버웍스관리용(삼안) ORGANIZATION 9caf62e1-297d-4e8f-870b-61780998bbeb saman su1 private
89 818c856b-9545-442f-b827-d1c569f200b0 기술개발센터(조직도용) ORGANIZATION 9caf62e1-297d-4e8f-870b-61780998bbeb saman rnd-center public
90 93e1f3dc-503f-40f7-8342-31974b5dd33c 해외사업부 ORGANIZATION ec325e6f-4333-43ae-a52a-77b742eef7ed overseas-headquarters overseas-business public
91 ec325e6f-4333-43ae-a52a-77b742eef7ed 해외사업본부 ORGANIZATION 9caf62e1-297d-4e8f-870b-61780998bbeb saman overseas-headquarters public
92 5b531a28-a222-44de-87f4-8a0560685a25 수력부 ORGANIZATION f60d0b66-e0c8-46be-88dc-e8bc93b9e149 water-resources-hq hydropower public
93 6c1ed0b6-8141-4ff8-96fb-a84efbe3a2ce 수자원2부 ORGANIZATION f60d0b66-e0c8-46be-88dc-e8bc93b9e149 water-resources-hq water-resources-2 public
94 d59651ea-41d3-45ee-b519-537fc6a97e65 수자원1부 ORGANIZATION f60d0b66-e0c8-46be-88dc-e8bc93b9e149 water-resources-hq water-resources-1 public
95 f60d0b66-e0c8-46be-88dc-e8bc93b9e149 수자원본부 ORGANIZATION 9caf62e1-297d-4e8f-870b-61780998bbeb saman water-resources-hq public
96 1596d8d3-e92d-4c95-8e3e-56c824991ee9 물환경3부 ORGANIZATION 9b9b8b70-b058-4f22-a0c3-e63bc0c7e1b8 water-environment-hq water-environment-3 public
97 9ff11813-07ae-41d2-b2af-ac4038dafde2 물환경2부 ORGANIZATION 9b9b8b70-b058-4f22-a0c3-e63bc0c7e1b8 water-environment-hq water-environment-2 public
98 2e4d1c4f-f859-4bdc-8531-8c2f29d74cc0 물환경1부 ORGANIZATION 9b9b8b70-b058-4f22-a0c3-e63bc0c7e1b8 water-environment-hq water-environment-1 public
99 9b9b8b70-b058-4f22-a0c3-e63bc0c7e1b8 물환경본부 ORGANIZATION 9caf62e1-297d-4e8f-870b-61780998bbeb saman water-environment-hq public
100 25e9f426-8012-43bd-8d2f-35ad76f0236a 환경평가부 ORGANIZATION 7ee20c20-01a7-4e39-9a58-a2ffdad153dd railway-headquarters environment-assessment public
101 a4722e17-c3f9-49d9-8535-4c92d03ccaae 철도2부 ORGANIZATION 7ee20c20-01a7-4e39-9a58-a2ffdad153dd railway-headquarters railway-2 public
102 c826ecf7-c2f9-49a1-ab72-0413216999af 철도1부 ORGANIZATION 7ee20c20-01a7-4e39-9a58-a2ffdad153dd railway-headquarters railway-1 public
103 7ee20c20-01a7-4e39-9a58-a2ffdad153dd 철도본부 ORGANIZATION 9caf62e1-297d-4e8f-870b-61780998bbeb saman railway-headquarters public
104 a1bca214-1f8a-403d-8f0b-c6c3aa71bb5a 안전진단팀 ORGANIZATION 967f657c-04e2-49c1-9cb9-f6afba27c0fa structures safety-inspection public
105 967f657c-04e2-49c1-9cb9-f6afba27c0fa 구조부 ORGANIZATION dfdc84b7-87a1-4383-8df7-5e656193e79c road-headquarters structures public
106 c172de4d-1e61-47d5-9446-7bb36ea50063 교통계획부 ORGANIZATION dfdc84b7-87a1-4383-8df7-5e656193e79c road-headquarters transport-planning public
107 6760a731-d172-4bc4-8ed4-1b625752c4a2 지반터널부 ORGANIZATION dfdc84b7-87a1-4383-8df7-5e656193e79c road-headquarters geotech-tunnel public
108 0b07f990-3ddb-45fa-b3d1-6233e3580f71 도로부 ORGANIZATION dfdc84b7-87a1-4383-8df7-5e656193e79c road-headquarters road public
109 dfdc84b7-87a1-4383-8df7-5e656193e79c 도로본부 ORGANIZATION 9caf62e1-297d-4e8f-870b-61780998bbeb saman road-headquarters public
110 953a07f6-5ff9-46ed-9a16-bd3066ccec12 조경레저부 ORGANIZATION 95d10fc3-b5d1-4613-9b0a-f9e6cae90d83 land-development landscape-leisure public
111 ed9290e8-7296-47fc-b72f-6ccc45765d4e 도시개발부 ORGANIZATION 95d10fc3-b5d1-4613-9b0a-f9e6cae90d83 land-development urban-development public
112 02ab05a1-caa8-41c9-8c9a-91de614724fd 도시계획부 ORGANIZATION 95d10fc3-b5d1-4613-9b0a-f9e6cae90d83 land-development urban-planning public
113 95d10fc3-b5d1-4613-9b0a-f9e6cae90d83 국토개발본부 ORGANIZATION 9caf62e1-297d-4e8f-870b-61780998bbeb saman land-development public
114 f36e2211-8cfd-4813-8618-34e606fe73ac 항만부 ORGANIZATION cd10e627-808b-4c98-8fdd-d47c84ce57d6 plant-headquarters harbor public
115 fec713bf-ac20-4480-bd74-686a2e6d92b3 플랜트2부 ORGANIZATION cd10e627-808b-4c98-8fdd-d47c84ce57d6 plant-headquarters plant-2 public
116 ef3adb8e-3405-4027-a0f8-5d3d5bf11e84 플랜트1부 ORGANIZATION cd10e627-808b-4c98-8fdd-d47c84ce57d6 plant-headquarters plant-1 public
117 cd10e627-808b-4c98-8fdd-d47c84ce57d6 플랜트본부 ORGANIZATION 9caf62e1-297d-4e8f-870b-61780998bbeb saman plant-headquarters public
118 99735f38-47d6-490b-ad54-8090b3bef91f 호남지역총괄본부 ORGANIZATION 8bbd9aad-1b53-4504-befb-e8ae8792340f cm-division honam-headquarters public
119 8bbd9aad-1b53-4504-befb-e8ae8792340f CM사업부 ORGANIZATION 92b4fd3c-91ac-41d2-8757-9344861b97aa cm-headquarters cm-division public
120 92b4fd3c-91ac-41d2-8757-9344861b97aa CM본부 ORGANIZATION 9caf62e1-297d-4e8f-870b-61780998bbeb saman cm-headquarters public
121 fe58cad4-1fa6-4b87-a2eb-51b9ac41320e 사업개발실 ORGANIZATION 9caf62e1-297d-4e8f-870b-61780998bbeb saman business-development public
122 338fb8af-b594-4c41-9984-54ea4b37637b 안전품질관리실 ORGANIZATION 9caf62e1-297d-4e8f-870b-61780998bbeb saman safety-quality public
123 1edc196d-020c-4519-9ec4-3d23b99076e6 자산경영실 ORGANIZATION 9caf62e1-297d-4e8f-870b-61780998bbeb saman asset-management public
124 7adb550b-1756-49f6-b6cc-55b7b426ed52 인사총무부 ORGANIZATION 9bf67270-e15e-4278-b407-02dec5672876 business-strategy hr-admin public
125 01fcbee1-df33-4ee9-bf2b-6d9eb81917d9 대외협력팀 ORGANIZATION a16f49c4-6828-4fde-a164-43099c4560c4 planning external-relations public
126 cdc40c0b-f985-461a-be18-f8c8e82f31e8 재무회계팀 ORGANIZATION a16f49c4-6828-4fde-a164-43099c4560c4 planning finance public
127 c6aa2133-ded0-451c-b51b-27faa8b56507 PQ팀 ORGANIZATION a16f49c4-6828-4fde-a164-43099c4560c4 planning pq-team public
128 ca54cffe-ad30-4f9e-983a-88a85c70404d 업무팀 ORGANIZATION d656c134-a50b-43b9-8c2d-fb3738dd0f9f halla-mgmt-support-hq halla-operations public
129 a16f49c4-6828-4fde-a164-43099c4560c4 기획부 ORGANIZATION 9bf67270-e15e-4278-b407-02dec5672876 business-strategy planning public
130 9bf67270-e15e-4278-b407-02dec5672876 경영전략본부 ORGANIZATION 9caf62e1-297d-4e8f-870b-61780998bbeb saman business-strategy public
131 896da8ab-50b7-4a63-abbc-c85037b63acc 시공BIM ORGANIZATION 56cd0fd7-b62a-43c0-8db9-74a30468d7cb tdc construction-bim public
132 410a25b1-cd84-46d4-b4d6-8627b397ca42 스마트건설 ORGANIZATION 56cd0fd7-b62a-43c0-8db9-74a30468d7cb tdc smart-construction public
133 68c85ffe-5942-42e7-9785-b2c29b18ecb9 수자원 ORGANIZATION 56cd0fd7-b62a-43c0-8db9-74a30468d7cb tdc water-resources public
134 27683cb5-98ac-49cf-ac57-8157d7d2a663 PM ORGANIZATION 9e5919f5-0839-4d98-b2e8-dee2ef518b1d gsim-dev project-management public
135 39ad464a-8468-4acb-8f05-29bc828a1576 GSIM ORGANIZATION 9e5919f5-0839-4d98-b2e8-dee2ef518b1d gsim-dev gsim public
136 2501e4a7-bf41-48df-afae-76676e0169f1 bCMf ORGANIZATION 9e5919f5-0839-4d98-b2e8-dee2ef518b1d gsim-dev bcmf public
137 9e5919f5-0839-4d98-b2e8-dee2ef518b1d GSIM개발 ORGANIZATION 56cd0fd7-b62a-43c0-8db9-74a30468d7cb tdc gsim-dev public
138 b83c124a-8d5b-4b14-9347-9d1dca2b42fa 웹디자인 ORGANIZATION f8366e19-d767-46db-a789-49e36d88a8fc web-solutions web-design public
139 2403109e-1e43-4390-8d2a-6f0566be028e ERP ORGANIZATION f8366e19-d767-46db-a789-49e36d88a8fc web-solutions erp public
140 54986461-8cf1-4791-b130-8fecbff5f5a2 솔루션개발 ORGANIZATION f8366e19-d767-46db-a789-49e36d88a8fc web-solutions solution-dev public
141 f8366e19-d767-46db-a789-49e36d88a8fc 웹솔루션 ORGANIZATION 56cd0fd7-b62a-43c0-8db9-74a30468d7cb tdc web-solutions public
142 ec003372-962a-4d7b-b90a-edf9dfbb6eea Abut&시공통합관제 ORGANIZATION d394dcf2-d474-4059-9cbb-0fca343ec38c graphics abut-control public
143 52266543-a90b-4441-99c6-51f454b6059a EG-BIM Draw ORGANIZATION d394dcf2-d474-4059-9cbb-0fca343ec38c graphics eg-bim-draw public
144 1d74bebb-c5a1-49d4-bec4-90f0c89ad21f HmEG ORGANIZATION d394dcf2-d474-4059-9cbb-0fca343ec38c graphics hmeg public
145 87831866-07de-4e3c-b158-28188a3c1edb Modeler ORGANIZATION d394dcf2-d474-4059-9cbb-0fca343ec38c graphics modeler public
146 d394dcf2-d474-4059-9cbb-0fca343ec38c 그래픽스 ORGANIZATION 56cd0fd7-b62a-43c0-8db9-74a30468d7cb tdc graphics public
147 d43eed1b-3a36-4624-a040-7f2827016df6 Strana ORGANIZATION 56cd0fd7-b62a-43c0-8db9-74a30468d7cb tdc strana public
148 78f251f6-d35b-422d-92ab-7fabd80bef85 구조물S/W ORGANIZATION 56cd0fd7-b62a-43c0-8db9-74a30468d7cb tdc structural-software public
149 b06b8821-b74c-44d5-89c4-3a94e9979161 Watch BIM ORGANIZATION 9b211775-e6b6-4caf-8c98-91249186e3d9 infra-solution-dev watch-bim public
150 79923c99-1aab-4950-b69e-6312636f544f Primal 평면 ORGANIZATION 9b211775-e6b6-4caf-8c98-91249186e3d9 infra-solution-dev primal-plan public
151 28fba7fd-6af5-43c1-807b-fb97d6eafada Way Draw ORGANIZATION 9b211775-e6b6-4caf-8c98-91249186e3d9 infra-solution-dev way-draw public
152 0cec58e8-efbe-4f93-a734-d1b9cc7747b7 비탈면/구조물 ORGANIZATION 9b211775-e6b6-4caf-8c98-91249186e3d9 infra-solution-dev slope-structures public
153 9b211775-e6b6-4caf-8c98-91249186e3d9 인프라솔루션 개발 ORGANIZATION 56cd0fd7-b62a-43c0-8db9-74a30468d7cb tdc infra-solution-dev public
154 f39ef0c8-0ad0-49cd-97ff-b0672591cfe3 단지설계 개발 ORGANIZATION d015f4fc-6ce1-4258-be37-6c531cf75c6c cheonjijin site-design-dev public
155 35cc1fdf-6c0e-4b0e-8ce8-1adc918b8cbf 용지도셀 ORGANIZATION d015f4fc-6ce1-4258-be37-6c531cf75c6c cheonjijin land-map-cell public
156 c3e59dbc-a315-47f9-8787-2a792a311e32 천지인셀 ORGANIZATION d015f4fc-6ce1-4258-be37-6c531cf75c6c cheonjijin cheonjijin-cell public
157 d015f4fc-6ce1-4258-be37-6c531cf75c6c 천지인 ORGANIZATION 56cd0fd7-b62a-43c0-8db9-74a30468d7cb tdc cheonjijin public
158 61e9ed21-e100-475a-a1e6-bdb2a302db95 상하수도 ORGANIZATION 56cd0fd7-b62a-43c0-8db9-74a30468d7cb tdc water-sewer public
159 0e206c1f-8e9b-43c2-91ce-a17bf62854c8 단가산출 ORGANIZATION 0471c240-7080-4648-86a6-5fdecff9e148 cost-control cost-estimate public
160 bc903928-61cb-45a8-9b4a-794f62a9f8a6 공정관리 ORGANIZATION 0471c240-7080-4648-86a6-5fdecff9e148 cost-control schedule-control public
161 0471c240-7080-4648-86a6-5fdecff9e148 CC ORGANIZATION 56cd0fd7-b62a-43c0-8db9-74a30468d7cb tdc cost-control public
162 fcb4c9d3-ea2c-48c6-92cc-63706f53fa21 터널 ORGANIZATION 582972ce-6b06-48c8-aa30-31a5285e160e structural-division tunnel public
163 fab2c4ca-4081-4f64-924d-f32bdf2e61b4 CM기획 ORGANIZATION 582972ce-6b06-48c8-aa30-31a5285e160e structural-division cm-planning public
164 1e1a999a-38f0-40f1-ae89-62e6031a29e0 하부구조 ORGANIZATION 582972ce-6b06-48c8-aa30-31a5285e160e structural-division substructure public
165 9eb73493-8aa1-43d4-99a8-424b1a7d60be 구조물계획 ORGANIZATION 582972ce-6b06-48c8-aa30-31a5285e160e structural-division structure-planning public
166 3d147a08-00b9-47c7-940a-d75c36a6ce81 일반구조물 ORGANIZATION 582972ce-6b06-48c8-aa30-31a5285e160e structural-division structural-design public
167 1d39617e-8e50-4081-bcd5-551e7842f7b3 DfMA ORGANIZATION 582972ce-6b06-48c8-aa30-31a5285e160e structural-division dfma public
168 582972ce-6b06-48c8-aa30-31a5285e160e 일반구조물 div ORGANIZATION 56cd0fd7-b62a-43c0-8db9-74a30468d7cb tdc structural-division public
169 56cd0fd7-b62a-43c0-8db9-74a30468d7cb 기술개발센터 ORGANIZATION 5530ca6e-c5e6-4bf0-84d6-76c6a8fb70ee gpdtdc tdc internal
170 59b83b98-b604-4621-8527-b872be47accc 네이버웍스관리용(총센) ORGANIZATION 5530ca6e-c5e6-4bf0-84d6-76c6a8fb70ee gpdtdc su3 private
171 586434c5-3460-458b-b2e5-488b0e77fa21 솔루션통합 ORGANIZATION 761a8725-9c19-442c-986c-0319e33a5b1e gpd solution-integration public
172 df6a504d-75b9-4a21-a67e-1522cc18111c 협업증진 ORGANIZATION 761a8725-9c19-442c-986c-0319e33a5b1e gpd collaboration public
173 6f420ad4-2345-43b5-9c2d-7ba0dae8cae4 디자인기획 ORGANIZATION 761a8725-9c19-442c-986c-0319e33a5b1e gpd design-planning public
174 88d1d1ee-795a-4b67-a8e7-bb0d4d7ac49f ERP기획 ORGANIZATION 761a8725-9c19-442c-986c-0319e33a5b1e gpd erp-planning public
175 c6b1266c-564b-4543-baba-d78807a3d1b4 경영기획 ORGANIZATION 761a8725-9c19-442c-986c-0319e33a5b1e gpd management-planning public
176 3a660456-eceb-472b-a9a9-f2a5b0ce972b 기술기획 ORGANIZATION 761a8725-9c19-442c-986c-0319e33a5b1e gpd tech-planning public
177 556798a6-b45e-4822-b6f1-42046b6d0001 전산관리TF ORGANIZATION 761a8725-9c19-442c-986c-0319e33a5b1e gpd it-admin-tf internal
178 539f598d-e6f1-4fe2-be48-466448d8d803 인재성장 ORGANIZATION 761a8725-9c19-442c-986c-0319e33a5b1e gpd talent-growth public
179 761a8725-9c19-442c-986c-0319e33a5b1e 총괄기획실 ORGANIZATION 5530ca6e-c5e6-4bf0-84d6-76c6a8fb70ee gpdtdc gpd public
180 e57cb22c-383e-4489-8c2f-0c5431917e86 PTC COMPANY 96369f12-6b66-4b2a-a916-d1c99d326f02 baron-group ptc public
181 9607eb7b-04d2-42ab-80fe-780fe21c7e8f Personal PERSONAL personal 개인 사용자 기본 루트 테넌트 public
182 5a03efd2-e62f-4243-800d-58334bf48b2f 한라산업개발 ORGANIZATION 96369f12-6b66-4b2a-a916-d1c99d326f02 baron-group halla public
183 b2fcf17f-7085-4bfe-9663-d8a2f2f4b2d6 장헌산업 ORGANIZATION 96369f12-6b66-4b2a-a916-d1c99d326f02 baron-group jangheon-sanup public
184 c18a8284-0008-48aa-9cdf-9f47ab79a2a9 (주)장헌 ORGANIZATION 96369f12-6b66-4b2a-a916-d1c99d326f02 baron-group jangheon public
185 96369f12-6b66-4b2a-a916-d1c99d326f02 바론그룹 COMPANY_GROUP 038326b6-954a-48a7-a85f-efd83f62b82a hanmac-family baron-group 네이버웍스 바론그룹 BARONGROUP_DOMAIN_ID public
186 5530ca6e-c5e6-4bf0-84d6-76c6a8fb70ee 총괄기획&기술개발센터 COMPANY 038326b6-954a-48a7-a85f-efd83f62b82a hanmac-family gpdtdc 네이버웍스 총괄기획&기술개발센터 GPDTDC_DOMAIN_ID baroncs.co.kr public
187 369c1843-56af-4344-9c21-0e01197ab861 한맥기술 COMPANY 038326b6-954a-48a7-a85f-efd83f62b82a hanmac-family hanmac 네이버웍스 한맥 HANMAC_DOMAIN_ID hanmaceng.co.kr public
188 9caf62e1-297d-4e8f-870b-61780998bbeb 삼안 COMPANY 038326b6-954a-48a7-a85f-efd83f62b82a hanmac-family saman 네이버웍스 삼안 SAMAN_DOMAIN_ID samaneng.com public
189 038326b6-954a-48a7-a85f-efd83f62b82a 한맥가족 COMPANY_GROUP hanmac-family 한맥가족 기본 루트 테넌트 public

11
test.sh
View File

@@ -1,11 +0,0 @@
#!/bin/sh
export USERFRONT_FLUTTER_RUN_FLAGS=""
set -- flutter run \
--wasm \
${USERFRONT_FLUTTER_RUN_FLAGS:-} \
--no-web-resources-cdn
echo "Count: $#"
for arg in "$@"; do
echo "Arg: $arg"
done

View File

@@ -1,221 +0,0 @@
email,name,phone,role,tenant_slug,department,grade,position,jobTitle,employee_id,tenant_slug1,department1,grade1,position1,jobTitle1,employee_id1,sub_email
cyhan@samaneng.com,한치영,01041585840,super,rnd-saman,,,,,224382,tech-planning,,책임연구원,,,b24051,b24051@hanmaceng.co.kr
jhshin@samaneng.com,신지호,010-9268-7509,user,rnd-saman,,,,,209171,erp,,책임연구원,,,M20329,m20329@hanmaceng.co.kr
swbae@samaneng.com,배상우,010-4716-5624,user,rnd-saman,,,,,215032,water-sewer,,선임연구원,,,B22062,b22062@hanmaceng.co.kr
hspark1@samaneng.com,박현수,010-3898-1757,user,rnd-saman,,,,,207241,water-sewer,,수석연구원,팀장,,B19206,b19206@hanmaceng.co.kr
smyoo@samaneng.com,유승민,010-9242-2912,user,rnd-saman,,,,,222244,strana,,선임연구원,,,B22058,b22058@hanmaceng.co.kr
mjjeong1@samaneng.com,정명준,010-3062-2026,user,rnd-saman,,,,,216070,solution-dev,,책임연구원,,,M20330,m20330@hanmaceng.co.kr
hjkim3@samaneng.com,김형준,010-4850-8649,user,rnd-saman,,,,,216121,tdc,,수석연구원,,,B16212,hjkim3@hanmaceng.co.kr
ypshim@samaneng.com,심영표,010-3296-1788,user,rnd-saman,,,,,216164,dfma,,수석연구원,팀장,,B16216,ypshim@hanmaceng.co.kr
jnoh@samaneng.com,노준,010-9177-0523,user,rnd-saman,,,,,217155,slope-structures,,수석연구원,,,B17206,jnoh@hanmaceng.co.kr
dwahn@samaneng.com,안대욱,010-6424-1980,user,rnd-saman,,,,,217157,cheonjijin-cell,,책임연구원,,,B10201,dw6092@hanmaceng.co.kr
kwjeong@samaneng.com,정계완,010-2743-8814,user,rnd-saman,,,,,218001,structural-software,,수석연구원,팀장,,B17203,kyewan@hanmaceng.co.kr
mskim7@samaneng.com,김민성,010-7730-8174,user,rnd-saman,,,,,218002,graphics,,수석연구원,,,B16213,mskim@hanmaceng.co.kr
sjyou@samaneng.com,유석준,010-2067-4875,user,rnd-saman,,,,,218003,smart-construction,,수석연구원,,,B16214,sjyou@hanmaceng.co.kr
kjkim1@samaneng.com,김경종,010-9644-7401,user,rnd-saman,,,,,218005,strana,,선임연구원,,,B17315,kjkim@hanmaceng.co.kr
iwlee@samaneng.com,이인우,010-5001-5305,user,rnd-saman,,,,,218007,structural-software,,책임연구원,,,B16305,inwoo772@hanmaceng.co.kr
gbkim@samaneng.com,김규범,010-3341-8624,user,rnd-saman,,,,,218008,land-map-cell,,선임연구원,,,B17308,gyubeom627@hanmaceng.co.kr
yjlee3@samaneng.com,이연재,010-5276-3376,user,rnd-saman,,,,,218009,structural-software,,선임연구원,,,B17309,yeonjae52@hanmaceng.co.kr
itkim@samaneng.com,김일태,010-6500-6873,user,rnd-saman,,,,,218027,structure-planning,,수석연구원,팀장,,B18206,itkim@hanmaceng.co.kr
jychoi1@samaneng.com,최진영,010-8070-0952,user,rnd-saman,,,,,218118,hmeg,,선임연구원,,,B18311,jy_choi@hanmaceng.co.kr
bjkim2@samaneng.com,김병조,010-8592-7983,user,rnd-saman,,,,,218128,infra-bim2,,수석연구원,팀장,,B18212,bjkim@hanmaceng.co.kr
hklee@samaneng.com,이호경,010-4748-1103,user,rnd-saman,,,,,218141,strana,,수석연구원,팀장,,B18215,hklee@hanmaceng.co.kr
hsryu1@samaneng.com,류한솔,010-9955-1825,user,rnd-saman,,,,,218144,primal-plan,,책임연구원,,,B18213,hansol.ryu@hanmaceng.co.kr
hyshin@samaneng.com,신혜영,010-3595-3511,user,rnd-saman,,,,,218145,design-planning,,수석연구원,팀장,,B18214,shy0622@hanmaceng.co.kr
hsyu@samaneng.com,유효식,010-8885-1095,user,rnd-saman,,,,,218151,schedule-control,,책임연구원,,,B18313,hyosik914@hanmaceng.co.kr
hikim@samaneng.com,김현일,010-9491-7161,user,rnd-saman,,,,,219001,substructure,,수석연구원,팀장,,B19201,kajm77@hanmaceng.co.kr
bhyang1@samaneng.com,양병홍,010-6201-0523,user,rnd-saman,,,,,219018,tdc,,부사장,센터장,,B18202,b18202@hanmaceng.co.kr
eklee1@samaneng.com,이은구,010-5672-7889,user,rnd-saman,,,,,219072,water-resources,,책임연구원,팀장,,B19203,lek@hanmaceng.co.kr
wtshin@samaneng.com,신원태,010-2726-0728,user,rnd-saman,,,,,219080,schedule-control,,책임연구원,,,B19204,panic7ka@hanmaceng.co.kr
dwlee2@samaneng.com,이동원,010-2910-3133,user,rnd-saman,,,,,219152,structural-division,,수석연구원,디비전장,,B19309,dwlee2@hanmaceng.co.kr
mskim@samaneng.com,김명식,010-2289-5257,user,rnd-saman,,,,,219154,hmeg,,선임연구원,,,B19310,myungsik@hanmaceng.co.kr
wison@samaneng.com,손원일,010-2430-4219,user,rnd-saman,,,,,219155,site-design-dev,,책임연구원,,,B19311,wison@hanmaceng.co.kr
dhlee@samaneng.com,이동호,010-8708-6817,user,rnd-saman,,,,,220047,infra-bim2,,선임연구원,,,B22056,b22056@hanmaceng.co.kr
ysjang1@samaneng.com,장용섭,010-4701-1006,user,rnd-saman,,,,,220147,way-draw,,책임연구원,,,B20202,yongseop@hanmaceng.co.kr
jahan@samaneng.com,한지아,010-2584-3790,user,rnd-saman,,,,,222057,web-design,,책임연구원,,,B22001,b22001@hanmaceng.co.kr
shkwon@samaneng.com,권순호,010-4432-4117,user,rnd-saman,,,,,222059,design-planning,,연구원,,,B22003,b22003@hanmaceng.co.kr
dlyoo@samaneng.com,유달리,010-9007-9064,user,rnd-saman,,,,,220227,infra-bim3,,책임연구원,,,B20205,b20205@hanmaceng.co.kr
yhjung2@samaneng.com,정요한,010-8867-6046,user,rnd-saman,,,,,220234,cost-control,,수석연구원,팀장,,B20326,b20326@hanmaceng.co.kr
ygkim1@samaneng.com,김윤권,010-4131-1369,user,rnd-saman,,,,,220266,schedule-control,,책임연구원,,,B20333,b20333@hanmaceng.co.kr
jwlee1@samaneng.com,이재원,010-7766-4757,user,rnd-saman,,,,,220271,modeler,,선임연구원,,,B20336,b20336@hanmaceng.co.kr
jhlee2@samaneng.com,이주형,010-7511-5468,user,rnd-saman,,,,,221022,infra-bim2,,선임연구원,,,B21315,b21315@hanmaceng.co.kr
jslee1@samaneng.com,이진수,010-6409-6442,user,rnd-saman,,,,,221040,land-map-cell,,선임연구원,,,B21306,b21306@hanmaceng.co.kr
yski@samaneng.com,기윤서,010-6289-9782,user,rnd-saman,,,,,221052,bcmf,,수석연구원,,,M21309,m21309@hanmaceng.co.kr
kakang@samaneng.com,강근아,010-3066-9589,user,rnd-saman,,,,,221054,eg-bim-draw,,선임연구원,,,M21318,m21318@hanmaceng.co.kr
jwpark8@samaneng.com,박정우,010-4794-0596,user,rnd-saman,,,,,221055,gsim,,선임연구원,,,B21309,b21309@hanmaceng.co.kr
bckim@samaneng.com,김병철,010-3016-7065,user,rnd-saman,,,,,221064,erp,,선임연구원,,,B21319,b21319@hanmaceng.co.kr
jykang1@samaneng.com,강지영,010-3322-6664,user,rnd-saman,,,,,221067,cm-planning,,선임연구원,,,B21320,b21320@hanmaceng.co.kr
ehjung1@samaneng.com,정은혜,010-3378-1154,user,rnd-saman,,,,,221163,design-planning,,책임연구원,,,B21339,b21339@hanmaceng.co.kr
alhong@samaneng.com,홍아름,010-4070-1948,user,rnd-saman,,,,,221184,tech-planning,,수석연구원,,,B21344,b21344@hanmaceng.co.kr
thlee3@samaneng.com,이태훈,010-4527-8434,user,rnd-saman,,,,,221270,tech-planning,,선임연구원,,,B21364,b21364@hanmaceng.co.kr
jsyun@samaneng.com,윤준수,010-9877-8748,user,rnd-saman,,,,,221293,solution-integration,,선임연구원,,,B21367,b21367@hanmaceng.co.kr
sphwang@samaneng.com,황선필,010-5035-5239,user,rnd-saman,,,,,221292,cm-planning,,선임연구원,,,B21368,b21368@hanmaceng.co.kr
jwchoi3@samaneng.com,최정우,010-8963-5736,user,rnd-saman,,,,,221337,water-sewer,,책임연구원,,,B22055,b21316@hanmaceng.co.kr
ngkim@samaneng.com,김남걸,010-2262-5708,user,rnd-saman,,,,,222004,schedule-control,,수석연구원,,,B21372,b21372@hanmaceng.co.kr
yhchoi@samaneng.com,최용혁,010-8513-1451,user,rnd-saman,,,,,222010,structure-planning,,선임연구원,,,B21370,b21370@hanmaceng.co.kr
skkang@samaneng.com,강상구,010-9291-0264,user,rnd-saman,,,,,222060,cm-planning,,선임연구원,,,B22004,b22004@hanmaceng.co.kr
unhuh@samaneng.com,허유나,010-8870-9345,user,rnd-saman,,,,,222073,design-planning,,선임연구원,,,B22011,b22011@hanmaceng.co.kr
chlee@samaneng.com,이창효,010-8725-3372,user,rnd-saman,,,,,222078,dfma,,선임연구원,,,B22019,b22019@hanmaceng.co.kr
mkim2@samaneng.com,임민경,010-8209-9929,user,rnd-saman,,,,,222087,management-planning,,책임연구원,,,B22015,b21365@hanmaceng.co.kr
cichoi@samaneng.com,최창인,010-4645-2808,user,rnd-saman,,,,,222089,substructure,,책임연구원,,,B22016,b22016@hanmaceng.co.kr
hikim2@samaneng.com,김혜인,010-9510-3760,user,rnd-saman,,,,,222123,tech-planning,,선임연구원,,,B22027,b22027@hanmaceng.co.kr
sclee@samaneng.com,이수창,010-7622-2729,user,rnd-saman,,,,,222150,infra-bim1,,선임연구원,,,B22031,b22031@hanmaceng.co.kr
dhkim3@samaneng.com,김도현,010-9396-6726,user,rnd-saman,,,,,222152,bcmf,,선임연구원,,,B22039,b22039@hanmaceng.co.kr
sdjo@samaneng.com,조선두,010-2009-9705,user,rnd-saman,,,,,222155,cm-planning,,책임연구원,팀장,,B22042,b22042@hanmaceng.co.kr
sachoi@samaneng.com,최선아,010-6460-2728,user,rnd-saman,,,,,222156,management-planning,,책임연구원,,,B22036,b22036@hanmaceng.co.kr
yjahn2@samaneng.com,안용주,010-5433-0545,user,rnd-saman,,,,,222157,dfma,,책임연구원,,,B22037,b22037@hanmaceng.co.kr
smlee@samaneng.com,이수문,010-9229-3480,user,rnd-saman,,,,,222158,dfma,,수석연구원,,,B22035,b22035@hanmaceng.co.kr
tskim@samaneng.com,김태식A,010-9965-9940,user,rnd-saman,,,,,222182,design-planning,,책임연구원,,,B22046,b22046@hanmaceng.co.kr
jhkang@samaneng.com,강정훈,010-9891-8798,user,rnd-saman,,,,,222212,strana,,연구원,,,B22048,b22048@hanmaceng.co.kr
jhkim14@samaneng.com,김재현,010-2534-7837,user,rnd-saman,,,,,222231,watch-bim,,수석연구원,,,B22051,b22051@hanmaceng.co.kr
yjchoi1@samaneng.com,최윤진,010-2349-6687,user,rnd-saman,,,,,222240,way-draw,,연구원,,,B22052,b22052@hanmaceng.co.kr
wkkim@samaneng.com,김원기,010-4727-8530,user,rnd-saman,,,,,222242,infra-bim1,,책임연구원,,,B22057,
jhlee@samaneng.com,이준호,010-2514-6898,user,rnd-saman,,,,,223046,structural-software,,연구원,,,B23003,b23003@hanmaceng.co.kr
jhchoi3@samaneng.com,최진헌,010-8638-8079,user,rnd-saman,,,,,222272,strana,,선임연구원,,,B22063,b22063@hanmaceng.co.kr
hulee1@samaneng.com,이한울,010-9271-8997,user,rnd-saman,,,,,222294,web-design,,연구원,,,B22069,b22069@hanmaceng.co.kr
dwkim3@samaneng.com,김도우,010-5008-6104,user,rnd-saman,,,,,223004,cost-estimate,,연구원,,,B22073,b22073@hanmaceng.co.kr
mskim8@samaneng.com,김민수,010-4570-0179,user,rnd-saman,,,,,223006,construction-bim,,책임연구원,,,B22074,b22074@hanmaceng.co.kr
jhjeong1@samaneng.com,정주현,010-7566-8314,user,rnd-saman,,,,,223007,cheonjijin-cell,,연구원,,,B22076,b22076@hanmaceng.co.kr
scbaek@samaneng.com,백순철,010-9619-0437,user,rnd-saman,,,,,223045,cheonjijin-cell,,연구원,,,B23002,b23002@hanmaceng.co.kr
shyeom1@samaneng.com,염승호,010-8835-0501,user,rnd-saman,,,,,223070,solution-integration,,수석연구원,,,B23008,b23008@hanmaceng.co.kr
jskim1@samaneng.com,김진선,010-7415-8300,user,rnd-saman,,,,,223158,solution-dev,,선임연구원,,,B23033,b23033@hanmaceng.co.kr
hyma@samaneng.com,마희연,010-8213-7601,user,rnd-saman,,,,,223089,design-planning,,선임연구원,,,B23015,b23015@hanmaceng.co.kr
dwjung@samaneng.com,정두휘,010-5521-6160,user,rnd-saman,,,,,223099,design-planning,,연구원,,,B23014,b23014@hanmaceng.co.kr
gshong@samaneng.com,홍길수,010-6641-0857,user,rnd-saman,,,,,223100,modeler,,연구원,,,B23019,b23019@hanmaceng.co.kr
marco@samaneng.com,마르코,010-6662-1599,user,rnd-saman,,,,,223105,strana,,선임연구원,,,B23020,b23020@hanmaceng.co.kr
hjjeong1@samaneng.com,정호진,010-7332-8456,user,rnd-saman,,,,,223114,strana,,연구원,,,B23022,b23022@hanmaceng.co.kr
yjlee2@samaneng.com,이예진,010-9262-7530,user,rnd-saman,,,,,223123,design-planning,,선임연구원,,,B23028,b23028@hanmaceng.co.kr
swpark@samaneng.com,박승우,010-5482-6617,user,rnd-saman,,,,,223195,abut-control,,연구원,,,B23038,b23038@hanmaceng.co.kr
hwji@samaneng.com,지현욱,010-9228-8426,user,rnd-saman,,,,,223134,water-resources,,책임연구원,,,B23025,b23025@hanmaceng.co.kr
swseo@samaneng.com,서승완,010-3245-1363,user,rnd-saman,,,,,223135,erp,,선임연구원,,,B23030,b23030@hanmaceng.co.kr
jykim4@samaneng.com,김주영,010-3855-2839,user,rnd-saman,,,,,223138,structural-design,,선임연구원,,,B23031,b23031@hanmaceng.co.kr
jglee1@samaneng.com,이정곤,010-3958-4115,user,rnd-saman,,,,,223184,cost-estimate,,책임연구원,,,B23036,b23036@hanmaceng.co.kr
hmin@samaneng.com,민홍,010-8654-5461,user,rnd-saman,,,,,223313,gsim,,선임연구원,,,B23055,b23055@hanmaceng.co.kr
hwan@samaneng.com,안효원,010-3358-4260,user,rnd-saman,,,,,223228,infra-bim1,,선임연구원,,,B23040,b23040@hanmaceng.co.kr
sihan@samaneng.com,한성일,010-4322-1100,user,rnd-saman,,,,,223226,abut-control,,책임연구원,,,B23042,b23042@hanmaceng.co.kr
jhkim25@samaneng.com,김재환,010-8962-3743,user,rnd-saman,,,,,223229,structural-design,,책임연구원,,,B23041,b23041@hanmaceng.co.kr
gylee1@samaneng.com,이가연,010-2430-5102,user,rnd-saman,,,,,223269,slope-structures,,연구원,,,B23047,b23047@hanmaceng.co.kr
yskim3@samaneng.com,김예서,010-9167-6132,user,rnd-saman,,,,,223280,land-map-cell,,연구원,,,B23051,b23051@hanmaceng.co.kr
jhpyo@samaneng.com,표재학,010-2522-4984,user,rnd-saman,,,,,223281,primal-plan,,연구원,,,B23052,b23052@hanmaceng.co.kr
sjkim6@samaneng.com,김신지,010-7667-8256,user,rnd-saman,,,,,223361,tech-planning,,연구원,,,B23064,b23064@hanmaceng.co.kr
jschoi@samaneng.com,최지수,010-3557-3726,user,rnd-saman,,,,,223385,water-sewer,,연구원,,,B23068,b23068@hanmaceng.co.kr
jsuhm@samaneng.com,엄지숙,010-5399-9030,user,rnd-saman,,,,,224048,eg-bim-draw,,책임연구원,,,B23072,b23072@hanmaceng.co.kr
kbpark@samaneng.com,박경빈,010-9811-7018,user,rnd-saman,,,,,224053,watch-bim,,연구원,,,B24004,b24004@hanmaceng.co.kr
hkyoon@samaneng.com,윤현경,010-4947-0798,user,rnd-saman,,,,,224057,structure-planning,,선임연구원,,,B24005,b24005@hanmaceng.co.kr
jepark1@samaneng.com,박지은,010-3738-7186,user,rnd-saman,,,,,224058,project-management,,연구원,,,B24006,b24006@hanmaceng.co.kr
kmlee1@samaneng.com,이경민,010-3409-1237,user,rnd-saman,,,,,224069,tech-planning,,선임연구원,,,B24009,b24009@hanmaceng.co.kr
sylim1@samaneng.com,임성엽,010-5702-1213,user,rnd-saman,,,,,224070,land-map-cell,,선임연구원,,,B24011,b24011@hanmaceng.co.kr
jgjeon@samaneng.com,전제경,010-3343-5898,user,rnd-saman,,,,,224091,cheonjijin-cell,,연구원,,,B24013,b24013@hanmaceng.co.kr
hgjang@samaneng.com,장한규,010-7561-3369,user,rnd-saman,,,,,224080,dfma,,연구원,,,B24010,b24010@hanmaceng.co.kr
dwham@samaneng.com,함도원,010-7557-2285,user,rnd-saman,,,,,224106,infra-bim3,,연구원,,,B24018,b24018@hanmaceng.co.kr
grmin@samaneng.com,민경록,010-3272-0097,user,rnd-saman,,,,,224234,hmeg,,연구원,,,B24033,b24033@hanmaceng.co.kr
hklee2@samaneng.com,이현경,010-2687-3453,user,rnd-saman,,,,,224265,site-design-dev,,연구원,,,B24035,b24035@hanmaceng.co.kr
hsjin@samaneng.com,진희성,010-6773-0063,user,rnd-saman,,,,,224291,infra-bim1,,연구원,,,B24039,b24039@hanmaceng.co.kr
gakim@samaneng.com,김근아,010-6301-3072,user,rnd-saman,,,,,224286,site-design-dev,,연구원,,,B24038,b24038@hanmaceng.co.kr
jgbyun@samaneng.com,변정안,010-2499-5922,user,rnd-saman,,,,,224361,dfma,,선임연구원,,,B24046,b24046@hanmaceng.co.kr
mspark@samaneng.com,박민선,010-3716-3845,user,rnd-saman,,,,,224353,tunnel,,연구원,,,B24044,b24044@hanmaceng.co.kr
hyhwang@samaneng.com,황호연,010-4927-3201,user,rnd-saman,,,,,224363,water-resources,,연구원,,,B24047,b24047@hanmaceng.co.kr
smlee2@samaneng.com,이상목,010-3470-9973,user,rnd-saman,,,,,224371,tunnel,,연구원,,,B24048,b24048@hanmaceng.co.kr
dhhan1@samaneng.com,한동현,010-3606-0738,user,rnd-saman,,,,,224385,infra-bim2,,연구원,,,B24052,b24052@hanmaceng.co.kr
jhchoi6@samaneng.com,최준호,010-9174-3191,user,rnd-saman,,,,,224394,gsim,,연구원,,,B24057,b24057@hanmaceng.co.kr
mjlee@samaneng.com,이민지,010-3904-5527,user,rnd-saman,,,,,224392,substructure,,연구원,,,B24054,b24054@hanmaceng.co.kr
mjjeong2@samaneng.com,정미정,010-4299-6544,user,rnd-saman,,,,,224391,structure-planning,,연구원,,,B24055,b24055@hanmaceng.co.kr
mklee@samaneng.com,이민규,010-6243-3767,user,rnd-saman,,,,,224398,abut-control,,연구원,,,B24058,b24058@hanmaceng.co.kr
anlee@samaneng.com,이에녹,010-3301-7191,user,rnd-saman,,,,,224402,infra-bim2,,연구원,,,B24060,b24060@hanmaceng.co.kr
bshan@samaneng.com,한반석,010-5052-1706,user,rnd-saman,,,,,225025,infra-bim3,,연구원,,,B25002,b25002@hanmaceng.co.kr
hckim4@samaneng.com,김희철,010-5012-8456,user,rnd-saman,,,,,225083,water-resources,,연구원,,,B25004,b25004@hanmaceng.co.kr
swpark2@samaneng.com,박성원,010-5672-0355,user,rnd-saman,,,,,225084,infra-bim2,,연구원,,,B25003,b25003@hanmaceng.co.kr
yjsung@samaneng.com,성유정,010-8976-2264,user,rnd-saman,,,,,225099,infra-bim1,,연구원,,,B25009,b25009@hanmaceng.co.kr
sjyou1@samaneng.com,유서진,010-8703-8014,user,rnd-saman,,,,,225100,infra-bim3,,연구원,,,B25010,b25010@hanmaceng.co.kr
gukim@samaneng.com,김건우A,010-6643-0460,user,rnd-saman,,,,,225105,gsim,,연구원,,,B25013,b25013@hanmaceng.co.kr
sykim3@samaneng.com,김성엽,010-3818-8608,user,rnd-saman,,,,,225110,infra-bim3,,선임연구원,,,B25011,b25011@hanmaceng.co.kr
jskwon@samaneng.com,권장승,010-7176-7142,user,rnd-saman,,,,,225111,infra-bim1,,연구원,,,B25014,b25014@hanmaceng.co.kr
jyjung1@samaneng.com,정지윤,010-7132-6329,user,rnd-saman,,,,,225140,design-planning,,연구원,,,B25017,b25017@hanmaceng.co.kr
jwjeong1@samaneng.com,정진우,010-5438-6084,user,rnd-saman,,,,,225122,hmeg,,연구원,,,B25016,b25016@hanmaceng.co.kr
cwshin@samaneng.com,신찬웅,010-5538-6590,user,rnd-saman,,,,,225141,watch-bim,,연구원,,,B25018,b25018@hanmaceng.co.kr
jskim2@samaneng.com,김종석,010-9458-1138,user,rnd-saman,,,,,225156,site-design-dev,,선임연구원,,,B25020,b25020@hanmaceng.co.kr
shpark10@samaneng.com,박석현,010-9252-6709,user,rnd-saman,,,,,225161,infra-bim1,,연구원,,,B25021,b25021@hanmaceng.co.kr
hjjung1@samaneng.com,정학재,010-9285-9318,user,rnd-saman,,,,,225162,infra-bim2,,연구원,,,B25022,b25022@hanmaceng.co.kr
hrlee1@samaneng.com,이해랑,010-8628-0094,user,rnd-saman,,,,,225175,modeler,,연구원,,,B25023,b25023@hanmaceng.co.kr
jhsim@samaneng.com,심재훈,010-6633-3366,user,rnd-saman,,,,,225183,tunnel,,수석연구원,,,B25025,b25025@hanmaceng.co.kr
shkim4@samaneng.com,김수현,010-5645-5153,user,rnd-saman,,,,,225215,design-planning,,선임연구원,,,B25027,b25027@hanmaceng.co.kr
smbaek@samaneng.com,백승민,010-7156-8542,user,rnd-saman,,,,,225319,hmeg,,책임연구원,,,B25035,b25035@hanmaceng.co.kr
swpark3@samaneng.com,박상원,010-4794-0148,user,rnd-saman,,,,,225336,cm-planning,,연구원,,,B25036,b25036@hanmaceng.co.kr
smyoun@samaneng.com,윤석무,010-9780-8901,user,rnd-saman,,,,,226049,solution-dev,,연구원,,,B26002,b26002@hanmaceng.co.kr
jhpark4@samaneng.com,박종혁,010-4211-2090,user,rnd-saman,,,,,226072,infra-bim2,,연구원,,,B26003,b26003@hanmaceng.co.kr
dhhong@samaneng.com,홍덕현,010-5360-7314,user,rnd-saman,,,,,226073,structural-design,,연구원,,,B26004,b26004@hanmaceng.co.kr
twchung@hanmaceng.co.kr,정태원,010-2362-3668,user,rnd-hanmac,,,,,twchung,tdc,,사장,,,M21201,ctw@hanmaceng.co.kr
shkim13@hanmaceng.co.kr,김승호,010-4753-3240,user,rnd-hanmac,,,,,shkim13,substructure,,수석연구원,,,M02248,soo98soo@hanmaceng.co.kr
jhkim32@hanmaceng.co.kr,김정훈,010-9152-7409,user,rnd-hanmac,,,,,jhkim32,infra-solution,,수석연구원,디비전장,,M04308,hunsing@hanmaceng.co.kr
khseok@hanmaceng.co.kr,곽현석,010-3280-3609,user,rnd-hanmac,,,,,khseok,structure-planning,,수석연구원,,,M06309,hyunss97@hanmaceng.co.kr
eshwang1@hanmaceng.co.kr,황은식,010-8792-9303,user,rnd-hanmac,,,,,eshwang1,infra-bim1,,수석연구원,팀장,,M07302,bobos1101@hanmaceng.co.kr
jjpyo@hanmaceng.co.kr,표종진,010-6406-1225,user,rnd-hanmac,,,,,jjpyo,infra-bim2,,수석연구원,,,M08301,piossy@hanmaceng.co.kr
hslee5@hanmaceng.co.kr,이호성,010-8622-3403,user,rnd-hanmac,,,,,hslee5,gsim-dev,,수석연구원,팀장,,M08303,jpsaviola@hanmaceng.co.kr
hylee4@hanmaceng.co.kr,이화영,010-4720-8841,user,rnd-hanmac,,,,,hylee4,tunnel,,수석연구원,팀장,,M12205,leehy@hanmaceng.co.kr
bjshin@hanmaceng.co.kr,신봉진,010-7189-4043,user,rnd-hanmac,,,,,bjshin,cheonjijin-cell,,수석연구원,,,M17203,bjshin@hanmaceng.co.kr
mjkang4@hanmaceng.co.kr,강명진,010-5158-3696,user,rnd-hanmac,,,,,mjkang4,cheonjijin,,수석연구원,팀장,,M17205,mjkang@hanmaceng.co.kr
msoh1@hanmaceng.co.kr,오문성,010-3319-7853,user,rnd-hanmac,,,,,msoh1,cost-estimate,,수석연구원,,,M18201,ohmunseong@hanmaceng.co.kr
swkim3@pre-cast.co.kr,김상욱,010-4857-3636,user,rnd-baron,,,,,swkim3,structural-design,,수석연구원,팀장,,P11202,p11202@hanmaceng.co.kr
yhkim8@brsw.kr,김윤하,010-3322-7515,user,rnd-baron,,,,,yhkim8,web-solutions,,수석연구원,팀장,,T03225,kyh@hanmaceng.co.kr
mnyoun@hanmaceng.co.kr,문남연,010-4534-4443,user,rnd-hanmac,,,,,mnyoun,infra-solution-dev,,수석연구원,팀장,,T04306,ace97@hanmaceng.co.kr
jgchoi@hanmaceng.co.kr,최정균,010-6737-9212,user,rnd-hanmac,,,,,jgchoi,construction-bim,,책임연구원,,,M26013,b21366@hanmaceng.co.kr
jwkim9@hanmaceng.co.kr,김지웅,010-4714-8160,user,rnd-hanmac,,,,,jwkim9,structural-software,,책임연구원,,,B13301,b13301@hanmaceng.co.kr
jychoi4@hanmaceng.co.kr,최준영,010-3156-1423,user,rnd-hanmac,,,,,jychoi4,eg-bim-draw,,책임연구원,,,B17314,cjy627@hanmaceng.co.kr
sykim5@brsw.kr,김세열,010-9122-6487,user,rnd-baron,,,,,sykim5,structural-software,,책임연구원,,,J15306,j15306@hanmaceng.co.kr
ktlee1@hanmaceng.co.kr,이광태,010-9863-1108,user,rnd-hanmac,,,,,ktlee1,infra-bim1,,책임연구원,,,M13301,ktqoqo@hanmaceng.co.kr
jykim7@pre-cast.co.kr,김지영,010-7412-1729,user,rnd-baron,,,,,jykim7,infra-bim3,,책임연구원,팀장,,M17208,jykim@hanmaceng.co.kr
ysmun@pre-cast.co.kr,문영석,010-2833-5718,user,rnd-baron,,,,,ysmun,hmeg,,선임연구원,,,B20309,munyeongseok@hanmaceng.co.kr
ghkim4@brsw.kr,김근형,010-2622-0967,user,rnd-baron,,,,,ghkim4,eg-bim-draw,,선임연구원,,,B20311,rmsgud1202@hanmaceng.co.kr
jkson@brsw.kr,손제근,010-6421-8791,user,rnd-baron,,,,,jkson,project-management,,선임연구원,,,B24022,b24022@hanmaceng.co.kr
jhmoon2@brsw.kr,문준혁,010-2345-3362,user,rnd-baron,,,,,jhmoon2,infra-bim1,,선임연구원,,,B25028,b25028@hanmaceng.co.kr
bslee2@brsw.kr,이배승,010-7583-8440,user,rnd-baron,,,,,bslee2,infra-bim1,,선임연구원,,,B25031,b25031@hanmaceng.co.kr
dhseo@brsw.kr,서동해,010-6289-9590,user,rnd-baron,,,,,dhseo,eg-bim-draw,,선임연구원,,,B24023,b24023@hanmaceng.co.kr
ybkim1@brsw.kr,김영배,010-6371-1318,user,rnd-baron,,,,,ybkim1,primal-plan,,선임연구원,,,B20327,b20327@hanmaceng.co.kr
jhchoi10@hanmaceng.co.kr,최정혁,010-4800-2603,user,rnd-hanmac,,,,,jhchoi10,tunnel,,선임연구원,,,M20212,jhchoi@hanmaceng.co.kr
hgkim5@hanmaceng.co.kr,김한결,010-8009-6172,user,rnd-hanmac,,,,,hgkim5,erp,,선임연구원,,,M22014,hgk121@hanmaceng.co.kr
cypark2@brsw.kr,박채영,010-4508-4006,user,rnd-baron,,,,,cypark2,watch-bim,,연구원,,,B24026,b24026@hanmaceng.co.kr
jylee8@brsw.kr,이지율,010-8652-9029,user,rnd-baron,,,,,jylee8,modeler,,연구원,,,B24021,b24021@hanmaceng.co.kr
shkang2@brsw.kr,강성호,010-2736-7419,user,rnd-baron,,,,,shkang2,way-draw,,연구원,,,B24024,b24024@hanmaceng.co.kr
yclee1@hanmaceng.co.kr,이예찬,010-4748-6225,user,rnd-hanmac,,,,,yclee1,primal-plan,,연구원,,,M24059,m24059@hanmaceng.co.kr
dgkwak@hanmaceng.co.kr,곽동권,010-6878-1926,user,rnd-hanmac,,,,,dgkwak,infra-bim2,,연구원,,,M24083,m24083@hanmaceng.co.kr
huyoon1@brsw.kr,윤현욱,010-7134-5068,user,rnd-baron,,,,,huyoon1,infra-bim1,,연구원,,,B25030,b25030@hanmaceng.co.kr
lhkim1@brsw.kr,김이훈,010-8778-0797,user,rnd-baron,,,,,lhkim1,infra-bim1,,연구원,,,B25032,b25032@hanmaceng.co.kr
ykshin@hanmaceng.co.kr,신영교,010-7567-2528,user,rnd-hanmac,,,,,ykshin,infra-bim2,,연구원,,,M24068,m24068@hanmaceng.co.kr
jtchoi@brsw.kr,최진태,010-6808-0921,user,rnd-baron,,,,,jtchoi,solution-dev,,연구원,,,B24032,b24032@hanmaceng.co.kr
myyang@brsw.kr,양미연,010-5523-5072,user,rnd-baron,,,,,myyang,web-design,,연구원,,,B25015,b25015@hanmaceng.co.kr
ymjo@brsw.kr,조용민,010-9490-9522,user,rnd-baron,,,,,ymjo,infra-bim1,,연구원,,,B25019,b25019@hanmaceng.co.kr
bwlee1@hanmaceng.co.kr,이병욱A,010-3286-4086,user,rnd-hanmac,,,,,bwlee1,infra-bim2,,연구원,,,M25013,m25013@hanmaceng.co.kr
bglee2@brsw.kr,이병권,010-5097-7600,user,rnd-baron,,,,,bglee2,erp,,연구원,,,B21369,b21369@hanmaceng.co.kr
jcjang@hanmaceng.co.kr,장종찬,010-5463-1677,user,rnd-hanmac,,,,,jcjang,gpd,,사장,,,M02210,jcjang67@hanmaceng.co.kr
hjkwon@brsw.kr,권혁진,010-8721-7453,user,rnd-baron,,,,,hjkwon,solution-integration,,수석연구원,,,B20304,cozyjin@hanmaceng.co.kr
thcho@brsw.kr,조태희,010-7588-8031,user,rnd-baron,,,,,thcho,talent-growth,,수석연구원,팀장,,B22040,b22040@hanmaceng.co.kr
wjkim@brsw.kr,김우진A,010-3218-8381,user,rnd-baron,,,,,wjkim,management-planning,,수석연구원,팀장,,J08305,j08305@hanmaceng.co.kr
hisung@hanmaceng.co.kr,성형일,010-2356-6633,user,rnd-hanmac,,,,,hisung,collaboration,,수석연구원,,,M06203,guddlf12@hanmaceng.co.kr
wgkim2@hanmaceng.co.kr,김원기,010-6283-6786,user,rnd-hanmac,,,,,wgkim2,tech-planning,,수석연구원,팀장,,M07318,kwongi79@hanmaceng.co.kr
hsryu2@brsw.kr,류호성,010-3371-5649,user,rnd-baron,,,,,hsryu2,erp-planning,,수석연구원,팀장,,M20331,m20331@hanmaceng.co.kr
wskim3@hanmaceng.co.kr,김원식,010-8755-6171,user,rnd-hanmac,,,,,wskim3,gpd,,전무이사,,,M19202,kws69@hanmaceng.co.kr
jhpark13@hanmaceng.co.kr,박주한,010-8955-3850,user,rnd-hanmac,,,,,jhpark13,collaboration,,책임연구원,,,M22006,m22006@hanmaceng.co.kr
hsmoon@hanmaceng.co.kr,문형석,010-9136-5338,user,rnd-hanmac,,,,,hsmoon,erp-planning,,책임연구원,,,M21420,moon79@hanmaceng.co.kr
smhan@hanmaceng.co.kr,한승민,010-3189-1514,user,rnd-hanmac,,,,,smhan,collaboration,,선임연구원,,,B23070,b23070@hanmaceng.co.kr
disong@brsw.kr,송대일,010-8627-0921,user,rnd-baron,,,,,disong,erp-planning,,선임연구원,,,B24014,b24014@hanmaceng.co.kr
wjryu@brsw.kr,류원준,010-9191-7771,user,rnd-baron,,,,,wjryu,talent-growth,,선임연구원,,,B24063,b24063@hanmaceng.co.kr
jykim8@hanmaceng.co.kr,김지영A,010-6389-0426,user,rnd-hanmac,,,,,jykim8,solution-integration,,선임연구원,,,M21430,kjy0426@hanmaceng.co.kr
jypark7@hanmaceng.co.kr,박지영,010-9055-4775,user,rnd-hanmac,,,,,jypark7,design-planning,,선임연구원,,,M21438,b23046@hanmaceng.co.kr
hrguk@pre-cast.co.kr,국혜림,010-6477-9711,user,rnd-baron,,,,,hrguk,management-planning,,선임연구원,,,B22038,b22038@hanmaceng.co.kr
hhchoi@brsw.kr,최현호,010-2279-3954,user,rnd-baron,,,,,hhchoi,tech-planning,,선임연구원,,,B22064,b22064@hanmaceng.co.kr
dhhwang@hanmaceng.co.kr,황동환,010-4242-6652,user,rnd-hanmac,,,,,dhhwang,tech-planning,,선임연구원,,,M19314,dhh12@hanmaceng.co.kr
khchoi4@brsw.kr,최근혜,010-3637-0646,user,rnd-baron,,,,,khchoi4,talent-growth,,선임연구원,,,B24008,b24008@hanmaceng.co.kr
biyun@brsw.kr,윤봄이,010-8482-2633,user,rnd-baron,,,,,biyun,design-planning,,선임연구원,,,B24016,b24016@hanmaceng.co.kr
mylee2@brsw.kr,이미영A,010-3007-3044,user,rnd-baron,,,,,mylee2,management-planning,,선임연구원,,,B22041,b22041@hanmaceng.co.kr
ojkwon1@hanmaceng.co.kr,권오재,010-9114-3943,user,rnd-hanmac,,,,,ojkwon1,erp-planning,,선임연구원,,,M24031,m24031@hanmaceng.co.kr
huchoi@pre-cast.co.kr,최혜은,010-3453-2360,user,rnd-baron,,,,,huchoi,design-planning,,선임연구원,,,B23060,b23060@hanmaceng.co.kr
sychae@brsw.kr,채선영,010-9523-0055,user,rnd-baron,,,,,sychae,design-planning,,선임연구원,,,B24027,b24027@hanmaceng.co.kr
yjkim7@hanmaceng.co.kr,김윤재,010-9747-9838,user,rnd-hanmac,,,,,yjkim7,management-planning,,선임연구원,,,M22047,gh.kim@hanmaceng.co.kr
yhchoi3@hanmaceng.co.kr,최영환,010-2905-0933,user,rnd-hanmac,,,,,yhchoi3,design-planning,,선임연구원,,,B16302,cyhwan0933@hanmaceng.co.kr
cyjo@brsw.kr,조찬영,010-6671-2879,user,rnd-baron,,,,,cyjo,tech-planning,,연구원,,,B24028,b24028@hanmaceng.co.kr
yykim@brsw.kr,김용연,010-2777-4695,user,rnd-baron,,,,,yykim,tech-planning,,연구원,,,B24053,b24053@hanmaceng.co.kr
sblee5@brsw.kr,이새봄,010-5704-9685,user,rnd-baron,,,,,sblee5,erp-planning,,연구원,,,B23018,b23018@hanmaceng.co.kr
shjeong@brsw.kr,정성호,010-5201-9028,user,rnd-baron,,,,,shjeong,talent-growth,,연구원,,,B24064,b24064@hanmaceng.co.kr
wgjoo@brsw.kr,주완기,010-4247-0144,user,rnd-baron,,,,,wgjoo,talent-growth,,연구원,,,B22067,b22067@hanmaceng.co.kr
syyang@brsw.kr,양숙영,010-7371-7662,user,rnd-baron,,,,,syyang,design-planning,,연구원,,,B24012,b24012@hanmaceng.co.kr
jskim12@brsw.kr,김정석,010-5209-7757,user,rnd-baron,,,,,jskim12,design-planning,,연구원,,,B24049,b24049@hanmaceng.co.kr
1 email name phone role tenant_slug department grade position jobTitle employee_id tenant_slug1 department1 grade1 position1 jobTitle1 employee_id1 sub_email
2 cyhan@samaneng.com 한치영 01041585840 super rnd-saman 224382 tech-planning 책임연구원 b24051 b24051@hanmaceng.co.kr
3 jhshin@samaneng.com 신지호 010-9268-7509 user rnd-saman 209171 erp 책임연구원 M20329 m20329@hanmaceng.co.kr
4 swbae@samaneng.com 배상우 010-4716-5624 user rnd-saman 215032 water-sewer 선임연구원 B22062 b22062@hanmaceng.co.kr
5 hspark1@samaneng.com 박현수 010-3898-1757 user rnd-saman 207241 water-sewer 수석연구원 팀장 B19206 b19206@hanmaceng.co.kr
6 smyoo@samaneng.com 유승민 010-9242-2912 user rnd-saman 222244 strana 선임연구원 B22058 b22058@hanmaceng.co.kr
7 mjjeong1@samaneng.com 정명준 010-3062-2026 user rnd-saman 216070 solution-dev 책임연구원 M20330 m20330@hanmaceng.co.kr
8 hjkim3@samaneng.com 김형준 010-4850-8649 user rnd-saman 216121 tdc 수석연구원 B16212 hjkim3@hanmaceng.co.kr
9 ypshim@samaneng.com 심영표 010-3296-1788 user rnd-saman 216164 dfma 수석연구원 팀장 B16216 ypshim@hanmaceng.co.kr
10 jnoh@samaneng.com 노준 010-9177-0523 user rnd-saman 217155 slope-structures 수석연구원 B17206 jnoh@hanmaceng.co.kr
11 dwahn@samaneng.com 안대욱 010-6424-1980 user rnd-saman 217157 cheonjijin-cell 책임연구원 B10201 dw6092@hanmaceng.co.kr
12 kwjeong@samaneng.com 정계완 010-2743-8814 user rnd-saman 218001 structural-software 수석연구원 팀장 B17203 kyewan@hanmaceng.co.kr
13 mskim7@samaneng.com 김민성 010-7730-8174 user rnd-saman 218002 graphics 수석연구원 B16213 mskim@hanmaceng.co.kr
14 sjyou@samaneng.com 유석준 010-2067-4875 user rnd-saman 218003 smart-construction 수석연구원 B16214 sjyou@hanmaceng.co.kr
15 kjkim1@samaneng.com 김경종 010-9644-7401 user rnd-saman 218005 strana 선임연구원 B17315 kjkim@hanmaceng.co.kr
16 iwlee@samaneng.com 이인우 010-5001-5305 user rnd-saman 218007 structural-software 책임연구원 B16305 inwoo772@hanmaceng.co.kr
17 gbkim@samaneng.com 김규범 010-3341-8624 user rnd-saman 218008 land-map-cell 선임연구원 B17308 gyubeom627@hanmaceng.co.kr
18 yjlee3@samaneng.com 이연재 010-5276-3376 user rnd-saman 218009 structural-software 선임연구원 B17309 yeonjae52@hanmaceng.co.kr
19 itkim@samaneng.com 김일태 010-6500-6873 user rnd-saman 218027 structure-planning 수석연구원 팀장 B18206 itkim@hanmaceng.co.kr
20 jychoi1@samaneng.com 최진영 010-8070-0952 user rnd-saman 218118 hmeg 선임연구원 B18311 jy_choi@hanmaceng.co.kr
21 bjkim2@samaneng.com 김병조 010-8592-7983 user rnd-saman 218128 infra-bim2 수석연구원 팀장 B18212 bjkim@hanmaceng.co.kr
22 hklee@samaneng.com 이호경 010-4748-1103 user rnd-saman 218141 strana 수석연구원 팀장 B18215 hklee@hanmaceng.co.kr
23 hsryu1@samaneng.com 류한솔 010-9955-1825 user rnd-saman 218144 primal-plan 책임연구원 B18213 hansol.ryu@hanmaceng.co.kr
24 hyshin@samaneng.com 신혜영 010-3595-3511 user rnd-saman 218145 design-planning 수석연구원 팀장 B18214 shy0622@hanmaceng.co.kr
25 hsyu@samaneng.com 유효식 010-8885-1095 user rnd-saman 218151 schedule-control 책임연구원 B18313 hyosik914@hanmaceng.co.kr
26 hikim@samaneng.com 김현일 010-9491-7161 user rnd-saman 219001 substructure 수석연구원 팀장 B19201 kajm77@hanmaceng.co.kr
27 bhyang1@samaneng.com 양병홍 010-6201-0523 user rnd-saman 219018 tdc 부사장 센터장 B18202 b18202@hanmaceng.co.kr
28 eklee1@samaneng.com 이은구 010-5672-7889 user rnd-saman 219072 water-resources 책임연구원 팀장 B19203 lek@hanmaceng.co.kr
29 wtshin@samaneng.com 신원태 010-2726-0728 user rnd-saman 219080 schedule-control 책임연구원 B19204 panic7ka@hanmaceng.co.kr
30 dwlee2@samaneng.com 이동원 010-2910-3133 user rnd-saman 219152 structural-division 수석연구원 디비전장 B19309 dwlee2@hanmaceng.co.kr
31 mskim@samaneng.com 김명식 010-2289-5257 user rnd-saman 219154 hmeg 선임연구원 B19310 myungsik@hanmaceng.co.kr
32 wison@samaneng.com 손원일 010-2430-4219 user rnd-saman 219155 site-design-dev 책임연구원 B19311 wison@hanmaceng.co.kr
33 dhlee@samaneng.com 이동호 010-8708-6817 user rnd-saman 220047 infra-bim2 선임연구원 B22056 b22056@hanmaceng.co.kr
34 ysjang1@samaneng.com 장용섭 010-4701-1006 user rnd-saman 220147 way-draw 책임연구원 B20202 yongseop@hanmaceng.co.kr
35 jahan@samaneng.com 한지아 010-2584-3790 user rnd-saman 222057 web-design 책임연구원 B22001 b22001@hanmaceng.co.kr
36 shkwon@samaneng.com 권순호 010-4432-4117 user rnd-saman 222059 design-planning 연구원 B22003 b22003@hanmaceng.co.kr
37 dlyoo@samaneng.com 유달리 010-9007-9064 user rnd-saman 220227 infra-bim3 책임연구원 B20205 b20205@hanmaceng.co.kr
38 yhjung2@samaneng.com 정요한 010-8867-6046 user rnd-saman 220234 cost-control 수석연구원 팀장 B20326 b20326@hanmaceng.co.kr
39 ygkim1@samaneng.com 김윤권 010-4131-1369 user rnd-saman 220266 schedule-control 책임연구원 B20333 b20333@hanmaceng.co.kr
40 jwlee1@samaneng.com 이재원 010-7766-4757 user rnd-saman 220271 modeler 선임연구원 B20336 b20336@hanmaceng.co.kr
41 jhlee2@samaneng.com 이주형 010-7511-5468 user rnd-saman 221022 infra-bim2 선임연구원 B21315 b21315@hanmaceng.co.kr
42 jslee1@samaneng.com 이진수 010-6409-6442 user rnd-saman 221040 land-map-cell 선임연구원 B21306 b21306@hanmaceng.co.kr
43 yski@samaneng.com 기윤서 010-6289-9782 user rnd-saman 221052 bcmf 수석연구원 M21309 m21309@hanmaceng.co.kr
44 kakang@samaneng.com 강근아 010-3066-9589 user rnd-saman 221054 eg-bim-draw 선임연구원 M21318 m21318@hanmaceng.co.kr
45 jwpark8@samaneng.com 박정우 010-4794-0596 user rnd-saman 221055 gsim 선임연구원 B21309 b21309@hanmaceng.co.kr
46 bckim@samaneng.com 김병철 010-3016-7065 user rnd-saman 221064 erp 선임연구원 B21319 b21319@hanmaceng.co.kr
47 jykang1@samaneng.com 강지영 010-3322-6664 user rnd-saman 221067 cm-planning 선임연구원 B21320 b21320@hanmaceng.co.kr
48 ehjung1@samaneng.com 정은혜 010-3378-1154 user rnd-saman 221163 design-planning 책임연구원 B21339 b21339@hanmaceng.co.kr
49 alhong@samaneng.com 홍아름 010-4070-1948 user rnd-saman 221184 tech-planning 수석연구원 B21344 b21344@hanmaceng.co.kr
50 thlee3@samaneng.com 이태훈 010-4527-8434 user rnd-saman 221270 tech-planning 선임연구원 B21364 b21364@hanmaceng.co.kr
51 jsyun@samaneng.com 윤준수 010-9877-8748 user rnd-saman 221293 solution-integration 선임연구원 B21367 b21367@hanmaceng.co.kr
52 sphwang@samaneng.com 황선필 010-5035-5239 user rnd-saman 221292 cm-planning 선임연구원 B21368 b21368@hanmaceng.co.kr
53 jwchoi3@samaneng.com 최정우 010-8963-5736 user rnd-saman 221337 water-sewer 책임연구원 B22055 b21316@hanmaceng.co.kr
54 ngkim@samaneng.com 김남걸 010-2262-5708 user rnd-saman 222004 schedule-control 수석연구원 B21372 b21372@hanmaceng.co.kr
55 yhchoi@samaneng.com 최용혁 010-8513-1451 user rnd-saman 222010 structure-planning 선임연구원 B21370 b21370@hanmaceng.co.kr
56 skkang@samaneng.com 강상구 010-9291-0264 user rnd-saman 222060 cm-planning 선임연구원 B22004 b22004@hanmaceng.co.kr
57 unhuh@samaneng.com 허유나 010-8870-9345 user rnd-saman 222073 design-planning 선임연구원 B22011 b22011@hanmaceng.co.kr
58 chlee@samaneng.com 이창효 010-8725-3372 user rnd-saman 222078 dfma 선임연구원 B22019 b22019@hanmaceng.co.kr
59 mkim2@samaneng.com 임민경 010-8209-9929 user rnd-saman 222087 management-planning 책임연구원 B22015 b21365@hanmaceng.co.kr
60 cichoi@samaneng.com 최창인 010-4645-2808 user rnd-saman 222089 substructure 책임연구원 B22016 b22016@hanmaceng.co.kr
61 hikim2@samaneng.com 김혜인 010-9510-3760 user rnd-saman 222123 tech-planning 선임연구원 B22027 b22027@hanmaceng.co.kr
62 sclee@samaneng.com 이수창 010-7622-2729 user rnd-saman 222150 infra-bim1 선임연구원 B22031 b22031@hanmaceng.co.kr
63 dhkim3@samaneng.com 김도현 010-9396-6726 user rnd-saman 222152 bcmf 선임연구원 B22039 b22039@hanmaceng.co.kr
64 sdjo@samaneng.com 조선두 010-2009-9705 user rnd-saman 222155 cm-planning 책임연구원 팀장 B22042 b22042@hanmaceng.co.kr
65 sachoi@samaneng.com 최선아 010-6460-2728 user rnd-saman 222156 management-planning 책임연구원 B22036 b22036@hanmaceng.co.kr
66 yjahn2@samaneng.com 안용주 010-5433-0545 user rnd-saman 222157 dfma 책임연구원 B22037 b22037@hanmaceng.co.kr
67 smlee@samaneng.com 이수문 010-9229-3480 user rnd-saman 222158 dfma 수석연구원 B22035 b22035@hanmaceng.co.kr
68 tskim@samaneng.com 김태식A 010-9965-9940 user rnd-saman 222182 design-planning 책임연구원 B22046 b22046@hanmaceng.co.kr
69 jhkang@samaneng.com 강정훈 010-9891-8798 user rnd-saman 222212 strana 연구원 B22048 b22048@hanmaceng.co.kr
70 jhkim14@samaneng.com 김재현 010-2534-7837 user rnd-saman 222231 watch-bim 수석연구원 B22051 b22051@hanmaceng.co.kr
71 yjchoi1@samaneng.com 최윤진 010-2349-6687 user rnd-saman 222240 way-draw 연구원 B22052 b22052@hanmaceng.co.kr
72 wkkim@samaneng.com 김원기 010-4727-8530 user rnd-saman 222242 infra-bim1 책임연구원 B22057
73 jhlee@samaneng.com 이준호 010-2514-6898 user rnd-saman 223046 structural-software 연구원 B23003 b23003@hanmaceng.co.kr
74 jhchoi3@samaneng.com 최진헌 010-8638-8079 user rnd-saman 222272 strana 선임연구원 B22063 b22063@hanmaceng.co.kr
75 hulee1@samaneng.com 이한울 010-9271-8997 user rnd-saman 222294 web-design 연구원 B22069 b22069@hanmaceng.co.kr
76 dwkim3@samaneng.com 김도우 010-5008-6104 user rnd-saman 223004 cost-estimate 연구원 B22073 b22073@hanmaceng.co.kr
77 mskim8@samaneng.com 김민수 010-4570-0179 user rnd-saman 223006 construction-bim 책임연구원 B22074 b22074@hanmaceng.co.kr
78 jhjeong1@samaneng.com 정주현 010-7566-8314 user rnd-saman 223007 cheonjijin-cell 연구원 B22076 b22076@hanmaceng.co.kr
79 scbaek@samaneng.com 백순철 010-9619-0437 user rnd-saman 223045 cheonjijin-cell 연구원 B23002 b23002@hanmaceng.co.kr
80 shyeom1@samaneng.com 염승호 010-8835-0501 user rnd-saman 223070 solution-integration 수석연구원 B23008 b23008@hanmaceng.co.kr
81 jskim1@samaneng.com 김진선 010-7415-8300 user rnd-saman 223158 solution-dev 선임연구원 B23033 b23033@hanmaceng.co.kr
82 hyma@samaneng.com 마희연 010-8213-7601 user rnd-saman 223089 design-planning 선임연구원 B23015 b23015@hanmaceng.co.kr
83 dwjung@samaneng.com 정두휘 010-5521-6160 user rnd-saman 223099 design-planning 연구원 B23014 b23014@hanmaceng.co.kr
84 gshong@samaneng.com 홍길수 010-6641-0857 user rnd-saman 223100 modeler 연구원 B23019 b23019@hanmaceng.co.kr
85 marco@samaneng.com 마르코 010-6662-1599 user rnd-saman 223105 strana 선임연구원 B23020 b23020@hanmaceng.co.kr
86 hjjeong1@samaneng.com 정호진 010-7332-8456 user rnd-saman 223114 strana 연구원 B23022 b23022@hanmaceng.co.kr
87 yjlee2@samaneng.com 이예진 010-9262-7530 user rnd-saman 223123 design-planning 선임연구원 B23028 b23028@hanmaceng.co.kr
88 swpark@samaneng.com 박승우 010-5482-6617 user rnd-saman 223195 abut-control 연구원 B23038 b23038@hanmaceng.co.kr
89 hwji@samaneng.com 지현욱 010-9228-8426 user rnd-saman 223134 water-resources 책임연구원 B23025 b23025@hanmaceng.co.kr
90 swseo@samaneng.com 서승완 010-3245-1363 user rnd-saman 223135 erp 선임연구원 B23030 b23030@hanmaceng.co.kr
91 jykim4@samaneng.com 김주영 010-3855-2839 user rnd-saman 223138 structural-design 선임연구원 B23031 b23031@hanmaceng.co.kr
92 jglee1@samaneng.com 이정곤 010-3958-4115 user rnd-saman 223184 cost-estimate 책임연구원 B23036 b23036@hanmaceng.co.kr
93 hmin@samaneng.com 민홍 010-8654-5461 user rnd-saman 223313 gsim 선임연구원 B23055 b23055@hanmaceng.co.kr
94 hwan@samaneng.com 안효원 010-3358-4260 user rnd-saman 223228 infra-bim1 선임연구원 B23040 b23040@hanmaceng.co.kr
95 sihan@samaneng.com 한성일 010-4322-1100 user rnd-saman 223226 abut-control 책임연구원 B23042 b23042@hanmaceng.co.kr
96 jhkim25@samaneng.com 김재환 010-8962-3743 user rnd-saman 223229 structural-design 책임연구원 B23041 b23041@hanmaceng.co.kr
97 gylee1@samaneng.com 이가연 010-2430-5102 user rnd-saman 223269 slope-structures 연구원 B23047 b23047@hanmaceng.co.kr
98 yskim3@samaneng.com 김예서 010-9167-6132 user rnd-saman 223280 land-map-cell 연구원 B23051 b23051@hanmaceng.co.kr
99 jhpyo@samaneng.com 표재학 010-2522-4984 user rnd-saman 223281 primal-plan 연구원 B23052 b23052@hanmaceng.co.kr
100 sjkim6@samaneng.com 김신지 010-7667-8256 user rnd-saman 223361 tech-planning 연구원 B23064 b23064@hanmaceng.co.kr
101 jschoi@samaneng.com 최지수 010-3557-3726 user rnd-saman 223385 water-sewer 연구원 B23068 b23068@hanmaceng.co.kr
102 jsuhm@samaneng.com 엄지숙 010-5399-9030 user rnd-saman 224048 eg-bim-draw 책임연구원 B23072 b23072@hanmaceng.co.kr
103 kbpark@samaneng.com 박경빈 010-9811-7018 user rnd-saman 224053 watch-bim 연구원 B24004 b24004@hanmaceng.co.kr
104 hkyoon@samaneng.com 윤현경 010-4947-0798 user rnd-saman 224057 structure-planning 선임연구원 B24005 b24005@hanmaceng.co.kr
105 jepark1@samaneng.com 박지은 010-3738-7186 user rnd-saman 224058 project-management 연구원 B24006 b24006@hanmaceng.co.kr
106 kmlee1@samaneng.com 이경민 010-3409-1237 user rnd-saman 224069 tech-planning 선임연구원 B24009 b24009@hanmaceng.co.kr
107 sylim1@samaneng.com 임성엽 010-5702-1213 user rnd-saman 224070 land-map-cell 선임연구원 B24011 b24011@hanmaceng.co.kr
108 jgjeon@samaneng.com 전제경 010-3343-5898 user rnd-saman 224091 cheonjijin-cell 연구원 B24013 b24013@hanmaceng.co.kr
109 hgjang@samaneng.com 장한규 010-7561-3369 user rnd-saman 224080 dfma 연구원 B24010 b24010@hanmaceng.co.kr
110 dwham@samaneng.com 함도원 010-7557-2285 user rnd-saman 224106 infra-bim3 연구원 B24018 b24018@hanmaceng.co.kr
111 grmin@samaneng.com 민경록 010-3272-0097 user rnd-saman 224234 hmeg 연구원 B24033 b24033@hanmaceng.co.kr
112 hklee2@samaneng.com 이현경 010-2687-3453 user rnd-saman 224265 site-design-dev 연구원 B24035 b24035@hanmaceng.co.kr
113 hsjin@samaneng.com 진희성 010-6773-0063 user rnd-saman 224291 infra-bim1 연구원 B24039 b24039@hanmaceng.co.kr
114 gakim@samaneng.com 김근아 010-6301-3072 user rnd-saman 224286 site-design-dev 연구원 B24038 b24038@hanmaceng.co.kr
115 jgbyun@samaneng.com 변정안 010-2499-5922 user rnd-saman 224361 dfma 선임연구원 B24046 b24046@hanmaceng.co.kr
116 mspark@samaneng.com 박민선 010-3716-3845 user rnd-saman 224353 tunnel 연구원 B24044 b24044@hanmaceng.co.kr
117 hyhwang@samaneng.com 황호연 010-4927-3201 user rnd-saman 224363 water-resources 연구원 B24047 b24047@hanmaceng.co.kr
118 smlee2@samaneng.com 이상목 010-3470-9973 user rnd-saman 224371 tunnel 연구원 B24048 b24048@hanmaceng.co.kr
119 dhhan1@samaneng.com 한동현 010-3606-0738 user rnd-saman 224385 infra-bim2 연구원 B24052 b24052@hanmaceng.co.kr
120 jhchoi6@samaneng.com 최준호 010-9174-3191 user rnd-saman 224394 gsim 연구원 B24057 b24057@hanmaceng.co.kr
121 mjlee@samaneng.com 이민지 010-3904-5527 user rnd-saman 224392 substructure 연구원 B24054 b24054@hanmaceng.co.kr
122 mjjeong2@samaneng.com 정미정 010-4299-6544 user rnd-saman 224391 structure-planning 연구원 B24055 b24055@hanmaceng.co.kr
123 mklee@samaneng.com 이민규 010-6243-3767 user rnd-saman 224398 abut-control 연구원 B24058 b24058@hanmaceng.co.kr
124 anlee@samaneng.com 이에녹 010-3301-7191 user rnd-saman 224402 infra-bim2 연구원 B24060 b24060@hanmaceng.co.kr
125 bshan@samaneng.com 한반석 010-5052-1706 user rnd-saman 225025 infra-bim3 연구원 B25002 b25002@hanmaceng.co.kr
126 hckim4@samaneng.com 김희철 010-5012-8456 user rnd-saman 225083 water-resources 연구원 B25004 b25004@hanmaceng.co.kr
127 swpark2@samaneng.com 박성원 010-5672-0355 user rnd-saman 225084 infra-bim2 연구원 B25003 b25003@hanmaceng.co.kr
128 yjsung@samaneng.com 성유정 010-8976-2264 user rnd-saman 225099 infra-bim1 연구원 B25009 b25009@hanmaceng.co.kr
129 sjyou1@samaneng.com 유서진 010-8703-8014 user rnd-saman 225100 infra-bim3 연구원 B25010 b25010@hanmaceng.co.kr
130 gukim@samaneng.com 김건우A 010-6643-0460 user rnd-saman 225105 gsim 연구원 B25013 b25013@hanmaceng.co.kr
131 sykim3@samaneng.com 김성엽 010-3818-8608 user rnd-saman 225110 infra-bim3 선임연구원 B25011 b25011@hanmaceng.co.kr
132 jskwon@samaneng.com 권장승 010-7176-7142 user rnd-saman 225111 infra-bim1 연구원 B25014 b25014@hanmaceng.co.kr
133 jyjung1@samaneng.com 정지윤 010-7132-6329 user rnd-saman 225140 design-planning 연구원 B25017 b25017@hanmaceng.co.kr
134 jwjeong1@samaneng.com 정진우 010-5438-6084 user rnd-saman 225122 hmeg 연구원 B25016 b25016@hanmaceng.co.kr
135 cwshin@samaneng.com 신찬웅 010-5538-6590 user rnd-saman 225141 watch-bim 연구원 B25018 b25018@hanmaceng.co.kr
136 jskim2@samaneng.com 김종석 010-9458-1138 user rnd-saman 225156 site-design-dev 선임연구원 B25020 b25020@hanmaceng.co.kr
137 shpark10@samaneng.com 박석현 010-9252-6709 user rnd-saman 225161 infra-bim1 연구원 B25021 b25021@hanmaceng.co.kr
138 hjjung1@samaneng.com 정학재 010-9285-9318 user rnd-saman 225162 infra-bim2 연구원 B25022 b25022@hanmaceng.co.kr
139 hrlee1@samaneng.com 이해랑 010-8628-0094 user rnd-saman 225175 modeler 연구원 B25023 b25023@hanmaceng.co.kr
140 jhsim@samaneng.com 심재훈 010-6633-3366 user rnd-saman 225183 tunnel 수석연구원 B25025 b25025@hanmaceng.co.kr
141 shkim4@samaneng.com 김수현 010-5645-5153 user rnd-saman 225215 design-planning 선임연구원 B25027 b25027@hanmaceng.co.kr
142 smbaek@samaneng.com 백승민 010-7156-8542 user rnd-saman 225319 hmeg 책임연구원 B25035 b25035@hanmaceng.co.kr
143 swpark3@samaneng.com 박상원 010-4794-0148 user rnd-saman 225336 cm-planning 연구원 B25036 b25036@hanmaceng.co.kr
144 smyoun@samaneng.com 윤석무 010-9780-8901 user rnd-saman 226049 solution-dev 연구원 B26002 b26002@hanmaceng.co.kr
145 jhpark4@samaneng.com 박종혁 010-4211-2090 user rnd-saman 226072 infra-bim2 연구원 B26003 b26003@hanmaceng.co.kr
146 dhhong@samaneng.com 홍덕현 010-5360-7314 user rnd-saman 226073 structural-design 연구원 B26004 b26004@hanmaceng.co.kr
147 twchung@hanmaceng.co.kr 정태원 010-2362-3668 user rnd-hanmac twchung tdc 사장 M21201 ctw@hanmaceng.co.kr
148 shkim13@hanmaceng.co.kr 김승호 010-4753-3240 user rnd-hanmac shkim13 substructure 수석연구원 M02248 soo98soo@hanmaceng.co.kr
149 jhkim32@hanmaceng.co.kr 김정훈 010-9152-7409 user rnd-hanmac jhkim32 infra-solution 수석연구원 디비전장 M04308 hunsing@hanmaceng.co.kr
150 khseok@hanmaceng.co.kr 곽현석 010-3280-3609 user rnd-hanmac khseok structure-planning 수석연구원 M06309 hyunss97@hanmaceng.co.kr
151 eshwang1@hanmaceng.co.kr 황은식 010-8792-9303 user rnd-hanmac eshwang1 infra-bim1 수석연구원 팀장 M07302 bobos1101@hanmaceng.co.kr
152 jjpyo@hanmaceng.co.kr 표종진 010-6406-1225 user rnd-hanmac jjpyo infra-bim2 수석연구원 M08301 piossy@hanmaceng.co.kr
153 hslee5@hanmaceng.co.kr 이호성 010-8622-3403 user rnd-hanmac hslee5 gsim-dev 수석연구원 팀장 M08303 jpsaviola@hanmaceng.co.kr
154 hylee4@hanmaceng.co.kr 이화영 010-4720-8841 user rnd-hanmac hylee4 tunnel 수석연구원 팀장 M12205 leehy@hanmaceng.co.kr
155 bjshin@hanmaceng.co.kr 신봉진 010-7189-4043 user rnd-hanmac bjshin cheonjijin-cell 수석연구원 M17203 bjshin@hanmaceng.co.kr
156 mjkang4@hanmaceng.co.kr 강명진 010-5158-3696 user rnd-hanmac mjkang4 cheonjijin 수석연구원 팀장 M17205 mjkang@hanmaceng.co.kr
157 msoh1@hanmaceng.co.kr 오문성 010-3319-7853 user rnd-hanmac msoh1 cost-estimate 수석연구원 M18201 ohmunseong@hanmaceng.co.kr
158 swkim3@pre-cast.co.kr 김상욱 010-4857-3636 user rnd-baron swkim3 structural-design 수석연구원 팀장 P11202 p11202@hanmaceng.co.kr
159 yhkim8@brsw.kr 김윤하 010-3322-7515 user rnd-baron yhkim8 web-solutions 수석연구원 팀장 T03225 kyh@hanmaceng.co.kr
160 mnyoun@hanmaceng.co.kr 문남연 010-4534-4443 user rnd-hanmac mnyoun infra-solution-dev 수석연구원 팀장 T04306 ace97@hanmaceng.co.kr
161 jgchoi@hanmaceng.co.kr 최정균 010-6737-9212 user rnd-hanmac jgchoi construction-bim 책임연구원 M26013 b21366@hanmaceng.co.kr
162 jwkim9@hanmaceng.co.kr 김지웅 010-4714-8160 user rnd-hanmac jwkim9 structural-software 책임연구원 B13301 b13301@hanmaceng.co.kr
163 jychoi4@hanmaceng.co.kr 최준영 010-3156-1423 user rnd-hanmac jychoi4 eg-bim-draw 책임연구원 B17314 cjy627@hanmaceng.co.kr
164 sykim5@brsw.kr 김세열 010-9122-6487 user rnd-baron sykim5 structural-software 책임연구원 J15306 j15306@hanmaceng.co.kr
165 ktlee1@hanmaceng.co.kr 이광태 010-9863-1108 user rnd-hanmac ktlee1 infra-bim1 책임연구원 M13301 ktqoqo@hanmaceng.co.kr
166 jykim7@pre-cast.co.kr 김지영 010-7412-1729 user rnd-baron jykim7 infra-bim3 책임연구원 팀장 M17208 jykim@hanmaceng.co.kr
167 ysmun@pre-cast.co.kr 문영석 010-2833-5718 user rnd-baron ysmun hmeg 선임연구원 B20309 munyeongseok@hanmaceng.co.kr
168 ghkim4@brsw.kr 김근형 010-2622-0967 user rnd-baron ghkim4 eg-bim-draw 선임연구원 B20311 rmsgud1202@hanmaceng.co.kr
169 jkson@brsw.kr 손제근 010-6421-8791 user rnd-baron jkson project-management 선임연구원 B24022 b24022@hanmaceng.co.kr
170 jhmoon2@brsw.kr 문준혁 010-2345-3362 user rnd-baron jhmoon2 infra-bim1 선임연구원 B25028 b25028@hanmaceng.co.kr
171 bslee2@brsw.kr 이배승 010-7583-8440 user rnd-baron bslee2 infra-bim1 선임연구원 B25031 b25031@hanmaceng.co.kr
172 dhseo@brsw.kr 서동해 010-6289-9590 user rnd-baron dhseo eg-bim-draw 선임연구원 B24023 b24023@hanmaceng.co.kr
173 ybkim1@brsw.kr 김영배 010-6371-1318 user rnd-baron ybkim1 primal-plan 선임연구원 B20327 b20327@hanmaceng.co.kr
174 jhchoi10@hanmaceng.co.kr 최정혁 010-4800-2603 user rnd-hanmac jhchoi10 tunnel 선임연구원 M20212 jhchoi@hanmaceng.co.kr
175 hgkim5@hanmaceng.co.kr 김한결 010-8009-6172 user rnd-hanmac hgkim5 erp 선임연구원 M22014 hgk121@hanmaceng.co.kr
176 cypark2@brsw.kr 박채영 010-4508-4006 user rnd-baron cypark2 watch-bim 연구원 B24026 b24026@hanmaceng.co.kr
177 jylee8@brsw.kr 이지율 010-8652-9029 user rnd-baron jylee8 modeler 연구원 B24021 b24021@hanmaceng.co.kr
178 shkang2@brsw.kr 강성호 010-2736-7419 user rnd-baron shkang2 way-draw 연구원 B24024 b24024@hanmaceng.co.kr
179 yclee1@hanmaceng.co.kr 이예찬 010-4748-6225 user rnd-hanmac yclee1 primal-plan 연구원 M24059 m24059@hanmaceng.co.kr
180 dgkwak@hanmaceng.co.kr 곽동권 010-6878-1926 user rnd-hanmac dgkwak infra-bim2 연구원 M24083 m24083@hanmaceng.co.kr
181 huyoon1@brsw.kr 윤현욱 010-7134-5068 user rnd-baron huyoon1 infra-bim1 연구원 B25030 b25030@hanmaceng.co.kr
182 lhkim1@brsw.kr 김이훈 010-8778-0797 user rnd-baron lhkim1 infra-bim1 연구원 B25032 b25032@hanmaceng.co.kr
183 ykshin@hanmaceng.co.kr 신영교 010-7567-2528 user rnd-hanmac ykshin infra-bim2 연구원 M24068 m24068@hanmaceng.co.kr
184 jtchoi@brsw.kr 최진태 010-6808-0921 user rnd-baron jtchoi solution-dev 연구원 B24032 b24032@hanmaceng.co.kr
185 myyang@brsw.kr 양미연 010-5523-5072 user rnd-baron myyang web-design 연구원 B25015 b25015@hanmaceng.co.kr
186 ymjo@brsw.kr 조용민 010-9490-9522 user rnd-baron ymjo infra-bim1 연구원 B25019 b25019@hanmaceng.co.kr
187 bwlee1@hanmaceng.co.kr 이병욱A 010-3286-4086 user rnd-hanmac bwlee1 infra-bim2 연구원 M25013 m25013@hanmaceng.co.kr
188 bglee2@brsw.kr 이병권 010-5097-7600 user rnd-baron bglee2 erp 연구원 B21369 b21369@hanmaceng.co.kr
189 jcjang@hanmaceng.co.kr 장종찬 010-5463-1677 user rnd-hanmac jcjang gpd 사장 M02210 jcjang67@hanmaceng.co.kr
190 hjkwon@brsw.kr 권혁진 010-8721-7453 user rnd-baron hjkwon solution-integration 수석연구원 B20304 cozyjin@hanmaceng.co.kr
191 thcho@brsw.kr 조태희 010-7588-8031 user rnd-baron thcho talent-growth 수석연구원 팀장 B22040 b22040@hanmaceng.co.kr
192 wjkim@brsw.kr 김우진A 010-3218-8381 user rnd-baron wjkim management-planning 수석연구원 팀장 J08305 j08305@hanmaceng.co.kr
193 hisung@hanmaceng.co.kr 성형일 010-2356-6633 user rnd-hanmac hisung collaboration 수석연구원 M06203 guddlf12@hanmaceng.co.kr
194 wgkim2@hanmaceng.co.kr 김원기 010-6283-6786 user rnd-hanmac wgkim2 tech-planning 수석연구원 팀장 M07318 kwongi79@hanmaceng.co.kr
195 hsryu2@brsw.kr 류호성 010-3371-5649 user rnd-baron hsryu2 erp-planning 수석연구원 팀장 M20331 m20331@hanmaceng.co.kr
196 wskim3@hanmaceng.co.kr 김원식 010-8755-6171 user rnd-hanmac wskim3 gpd 전무이사 M19202 kws69@hanmaceng.co.kr
197 jhpark13@hanmaceng.co.kr 박주한 010-8955-3850 user rnd-hanmac jhpark13 collaboration 책임연구원 M22006 m22006@hanmaceng.co.kr
198 hsmoon@hanmaceng.co.kr 문형석 010-9136-5338 user rnd-hanmac hsmoon erp-planning 책임연구원 M21420 moon79@hanmaceng.co.kr
199 smhan@hanmaceng.co.kr 한승민 010-3189-1514 user rnd-hanmac smhan collaboration 선임연구원 B23070 b23070@hanmaceng.co.kr
200 disong@brsw.kr 송대일 010-8627-0921 user rnd-baron disong erp-planning 선임연구원 B24014 b24014@hanmaceng.co.kr
201 wjryu@brsw.kr 류원준 010-9191-7771 user rnd-baron wjryu talent-growth 선임연구원 B24063 b24063@hanmaceng.co.kr
202 jykim8@hanmaceng.co.kr 김지영A 010-6389-0426 user rnd-hanmac jykim8 solution-integration 선임연구원 M21430 kjy0426@hanmaceng.co.kr
203 jypark7@hanmaceng.co.kr 박지영 010-9055-4775 user rnd-hanmac jypark7 design-planning 선임연구원 M21438 b23046@hanmaceng.co.kr
204 hrguk@pre-cast.co.kr 국혜림 010-6477-9711 user rnd-baron hrguk management-planning 선임연구원 B22038 b22038@hanmaceng.co.kr
205 hhchoi@brsw.kr 최현호 010-2279-3954 user rnd-baron hhchoi tech-planning 선임연구원 B22064 b22064@hanmaceng.co.kr
206 dhhwang@hanmaceng.co.kr 황동환 010-4242-6652 user rnd-hanmac dhhwang tech-planning 선임연구원 M19314 dhh12@hanmaceng.co.kr
207 khchoi4@brsw.kr 최근혜 010-3637-0646 user rnd-baron khchoi4 talent-growth 선임연구원 B24008 b24008@hanmaceng.co.kr
208 biyun@brsw.kr 윤봄이 010-8482-2633 user rnd-baron biyun design-planning 선임연구원 B24016 b24016@hanmaceng.co.kr
209 mylee2@brsw.kr 이미영A 010-3007-3044 user rnd-baron mylee2 management-planning 선임연구원 B22041 b22041@hanmaceng.co.kr
210 ojkwon1@hanmaceng.co.kr 권오재 010-9114-3943 user rnd-hanmac ojkwon1 erp-planning 선임연구원 M24031 m24031@hanmaceng.co.kr
211 huchoi@pre-cast.co.kr 최혜은 010-3453-2360 user rnd-baron huchoi design-planning 선임연구원 B23060 b23060@hanmaceng.co.kr
212 sychae@brsw.kr 채선영 010-9523-0055 user rnd-baron sychae design-planning 선임연구원 B24027 b24027@hanmaceng.co.kr
213 yjkim7@hanmaceng.co.kr 김윤재 010-9747-9838 user rnd-hanmac yjkim7 management-planning 선임연구원 M22047 gh.kim@hanmaceng.co.kr
214 yhchoi3@hanmaceng.co.kr 최영환 010-2905-0933 user rnd-hanmac yhchoi3 design-planning 선임연구원 B16302 cyhwan0933@hanmaceng.co.kr
215 cyjo@brsw.kr 조찬영 010-6671-2879 user rnd-baron cyjo tech-planning 연구원 B24028 b24028@hanmaceng.co.kr
216 yykim@brsw.kr 김용연 010-2777-4695 user rnd-baron yykim tech-planning 연구원 B24053 b24053@hanmaceng.co.kr
217 sblee5@brsw.kr 이새봄 010-5704-9685 user rnd-baron sblee5 erp-planning 연구원 B23018 b23018@hanmaceng.co.kr
218 shjeong@brsw.kr 정성호 010-5201-9028 user rnd-baron shjeong talent-growth 연구원 B24064 b24064@hanmaceng.co.kr
219 wgjoo@brsw.kr 주완기 010-4247-0144 user rnd-baron wgjoo talent-growth 연구원 B22067 b22067@hanmaceng.co.kr
220 syyang@brsw.kr 양숙영 010-7371-7662 user rnd-baron syyang design-planning 연구원 B24012 b24012@hanmaceng.co.kr
221 jskim12@brsw.kr 김정석 010-5209-7757 user rnd-baron jskim12 design-planning 연구원 B24049 b24049@hanmaceng.co.kr