1
0
forked from baron/baron-sso

chore: fix biome lint warnings & update Makefile

This commit is contained in:
2026-05-29 13:32:35 +09:00
parent deed33aad2
commit 00b89c04d6
22 changed files with 99 additions and 121 deletions

View File

@@ -276,18 +276,18 @@ code-check-front-lint:
@echo "==> adminfront biome lint/format check" @echo "==> adminfront biome lint/format check"
rm -rf adminfront/playwright-report adminfront/test-results rm -rf adminfront/playwright-report adminfront/test-results
cd adminfront && CI=true npx pnpm install --frozen-lockfile --ignore-scripts cd adminfront && CI=true npx pnpm install --frozen-lockfile --ignore-scripts
cd adminfront && npx biome check . --formatter-enabled=false --organize-imports-enabled=false cd adminfront && npx biome lint .
cd adminfront && npx biome check . --linter-enabled=false --organize-imports-enabled=false cd adminfront && npx biome format .
@echo "==> devfront biome lint/format check" @echo "==> devfront biome lint/format check"
rm -rf devfront/playwright-report devfront/test-results rm -rf devfront/playwright-report devfront/test-results
cd devfront && npm ci --ignore-scripts cd devfront && npm ci --ignore-scripts
cd devfront && npx biome check . --formatter-enabled=false --organize-imports-enabled=false cd devfront && npx biome lint .
cd devfront && npx biome check . --linter-enabled=false --organize-imports-enabled=false cd devfront && npx biome format .
@echo "==> orgfront biome lint/format check" @echo "==> orgfront biome lint/format check"
rm -rf orgfront/playwright-report orgfront/test-results rm -rf orgfront/playwright-report orgfront/test-results
cd orgfront && npm ci --ignore-scripts cd orgfront && npm ci --ignore-scripts
cd orgfront && npx biome check . --formatter-enabled=false --organize-imports-enabled=false cd orgfront && npx biome lint .
cd orgfront && npx biome check . --linter-enabled=false --organize-imports-enabled=false cd orgfront && npx biome format .
code-check-backend-tests: code-check-backend-tests:
@echo "==> backend tests" @echo "==> backend tests"

View File

@@ -3,7 +3,7 @@ import { createServer } from "node:http";
import { extname, join, normalize, resolve } from "node:path"; import { extname, join, normalize, resolve } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
const rootDir = fileURLToPath(new URL("..", import.meta.url)); const _rootDir = fileURLToPath(new URL("..", import.meta.url));
const distDir = resolve( const distDir = resolve(
process.env.ADMINFRONT_BUILD_OUT_DIR ?? "/tmp/baron-sso-adminfront-dist", process.env.ADMINFRONT_BUILD_OUT_DIR ?? "/tmp/baron-sso-adminfront-dist",
); );

View File

@@ -19,7 +19,6 @@ import { TenantProfilePage } from "../features/tenants/routes/TenantProfilePage"
import { TenantSchemaPage } from "../features/tenants/routes/TenantSchemaPage"; import { TenantSchemaPage } from "../features/tenants/routes/TenantSchemaPage";
import { TenantWorksmobilePage } from "../features/tenants/routes/TenantWorksmobilePage"; import { TenantWorksmobilePage } from "../features/tenants/routes/TenantWorksmobilePage";
import TenantUserGroupsTab from "../features/user-groups/routes/TenantUserGroupsTab"; import TenantUserGroupsTab from "../features/user-groups/routes/TenantUserGroupsTab";
import { UserGroupDetailPage } from "../features/user-groups/routes/UserGroupDetailPage";
import UserCreatePage from "../features/users/UserCreatePage"; import UserCreatePage from "../features/users/UserCreatePage";
import UserDetailPage from "../features/users/UserDetailPage"; import UserDetailPage from "../features/users/UserDetailPage";
import UserListPage from "../features/users/UserListPage"; import UserListPage from "../features/users/UserListPage";

View File

@@ -3,7 +3,6 @@ import type { AxiosError } from "axios";
import { Download, NotebookTabs, RefreshCw, Search } from "lucide-react"; import { Download, NotebookTabs, RefreshCw, Search } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { import {
formatAuditValue,
parseAuditDetails, parseAuditDetails,
resolveAuditAction, resolveAuditAction,
resolveAuditActor, resolveAuditActor,

View File

@@ -23,7 +23,6 @@ import {
fetchDataIntegrityReport, fetchDataIntegrityReport,
type RPUsageDailyMetric, type RPUsageDailyMetric,
type RPUsagePeriod, type RPUsagePeriod,
type TenantSummary,
} from "../../lib/adminApi"; } from "../../lib/adminApi";
import { t } from "../../lib/i18n"; import { t } from "../../lib/i18n";

View File

@@ -5,7 +5,6 @@ import {
Plus, Plus,
Search, Search,
ShieldCheck, ShieldCheck,
Trash2,
UserPlus, UserPlus,
Users, Users,
} from "lucide-react"; } from "lucide-react";
@@ -28,7 +27,6 @@ import {
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger,
} from "../../../components/ui/dialog"; } from "../../../components/ui/dialog";
import { Input } from "../../../components/ui/input"; import { Input } from "../../../components/ui/input";
import { import {
@@ -68,7 +66,7 @@ function mergePendingMembers(
export function TenantAdminsAndOwnersTab() { export function TenantAdminsAndOwnersTab() {
const auth = useAuth(); const auth = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const currentUserId = auth.user?.profile.sub; const _currentUserId = auth.user?.profile.sub;
const { tenantId: tenantIdParam } = useParams<{ tenantId: string }>(); const { tenantId: tenantIdParam } = useParams<{ tenantId: string }>();
const tenantId = tenantIdParam ?? ""; const tenantId = tenantIdParam ?? "";
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -187,7 +185,7 @@ export function TenantAdminsAndOwnersTab() {
), ),
); );
}, },
onError: (err: AxiosError<{ error?: string }>, userId, context) => { onError: (err: AxiosError<{ error?: string }>, _userId, context) => {
if (context?.previousOwners) { if (context?.previousOwners) {
queryClient.setQueryData( queryClient.setQueryData(
["tenant-owners", tenantId], ["tenant-owners", tenantId],
@@ -288,7 +286,7 @@ export function TenantAdminsAndOwnersTab() {
t("msg.admin.tenants.admins.remove_success", "권한이 회수되었습니다."), t("msg.admin.tenants.admins.remove_success", "권한이 회수되었습니다."),
); );
}, },
onError: (err: AxiosError<{ error?: string }>, userId, context) => { onError: (err: AxiosError<{ error?: string }>, _userId, context) => {
if (context?.previousAdmins) { if (context?.previousAdmins) {
queryClient.setQueryData( queryClient.setQueryData(
["tenant-admins", tenantId], ["tenant-admins", tenantId],
@@ -310,7 +308,7 @@ export function TenantAdminsAndOwnersTab() {
} }
}; };
const handleRemoveOwner = (userId: string, userName: string) => { const _handleRemoveOwner = (userId: string, userName: string) => {
if ( if (
window.confirm( window.confirm(
t( t(
@@ -324,7 +322,7 @@ export function TenantAdminsAndOwnersTab() {
} }
}; };
const handleRemoveAdmin = (userId: string, userName: string) => { const _handleRemoveAdmin = (userId: string, userName: string) => {
if ( if (
window.confirm( window.confirm(
t( t(

View File

@@ -1,7 +1,6 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Copy } from "lucide-react"; import { Copy } from "lucide-react";
import { Link, Outlet, useLocation, useParams } from "react-router-dom"; import { Link, Outlet, useLocation, useParams } from "react-router-dom";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button"; import { Button } from "../../../components/ui/button";
import { fetchMe, fetchTenant } from "../../../lib/adminApi"; import { fetchMe, fetchTenant } from "../../../lib/adminApi";
import { t } from "../../../lib/i18n"; import { t } from "../../../lib/i18n";

View File

@@ -38,7 +38,6 @@ import {
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger,
} from "../../../components/ui/dialog"; } from "../../../components/ui/dialog";
import { Input } from "../../../components/ui/input"; import { Input } from "../../../components/ui/input";
import { Label } from "../../../components/ui/label"; import { Label } from "../../../components/ui/label";
@@ -239,7 +238,7 @@ const UserGroupTreeNode: React.FC<UserGroupTreeNodeProps> = ({
function TenantGroupsPage() { function TenantGroupsPage() {
const params = useParams<{ tenantId: string }>(); const params = useParams<{ tenantId: string }>();
const tenantId = params.tenantId ?? ""; const tenantId = params.tenantId ?? "";
const queryClient = useQueryClient(); const _queryClient = useQueryClient();
const [newGroupName, setNewGroupName] = useState(""); const [newGroupName, setNewGroupName] = useState("");
const [newGroupDesc, setNewGroupNameDesc] = useState(""); const [newGroupDesc, setNewGroupNameDesc] = useState("");

View File

@@ -4,7 +4,6 @@ import {
useMutation, useMutation,
useQuery, useQuery,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { useVirtualizer } from "@tanstack/react-virtual";
import type { AxiosError } from "axios"; import type { AxiosError } from "axios";
import { import {
ArrowDown, ArrowDown,
@@ -14,7 +13,6 @@ import {
ChevronDown, ChevronDown,
ChevronRight, ChevronRight,
Download, Download,
ExternalLink,
FileSpreadsheet, FileSpreadsheet,
LayoutDashboard, LayoutDashboard,
List, List,
@@ -28,22 +26,13 @@ import {
import * as React from "react"; import * as React from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { PageHeader } from "../../../../../common/core/components/page"; import { PageHeader } from "../../../../../common/core/components/page";
import {
SortableTableHead,
sortableTableHeadBaseClassName,
sortableTableHeaderClassName,
} from "../../../../../common/core/components/sort";
import { import {
type SortConfig, type SortConfig,
type SortResolverMap, type SortResolverMap,
sortItems, sortItems,
toggleSort, toggleSort,
} from "../../../../../common/core/utils"; } from "../../../../../common/core/utils";
import { import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
commonStickyTableHeaderClass,
commonTableShellClass,
commonTableViewportClass,
} from "../../../../../common/ui/table";
import { RoleGuard } from "../../../components/auth/RoleGuard"; import { RoleGuard } from "../../../components/auth/RoleGuard";
import { Badge } from "../../../components/ui/badge"; import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button"; import { Button } from "../../../components/ui/button";
@@ -71,7 +60,6 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "../../../components/ui/dropdown-menu"; } from "../../../components/ui/dropdown-menu";
import { Input } from "../../../components/ui/input"; import { Input } from "../../../components/ui/input";
import { ScrollArea } from "../../../components/ui/scroll-area";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -79,7 +67,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "../../../components/ui/select"; } from "../../../components/ui/select";
import { Separator } from "../../../components/ui/separator";
import { Switch } from "../../../components/ui/switch"; import { Switch } from "../../../components/ui/switch";
import { import {
Table, Table,
@@ -132,10 +119,10 @@ import {
const tenantCSVTemplate = const tenantCSVTemplate =
"name,type,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type\n"; "name,type,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type\n";
const tenantPageSize = 500; const tenantPageSize = 500;
const tenantVirtualizationThreshold = 250; const _tenantVirtualizationThreshold = 250;
const tenantEstimatedRowHeight = 73; const _tenantEstimatedRowHeight = 73;
const tenantLoadAheadPx = 360; const _tenantLoadAheadPx = 360;
const tenantLoadAheadRows = 30; const _tenantLoadAheadRows = 30;
type TenantSortKey = keyof TenantSummary | "recursiveMemberCount"; type TenantSortKey = keyof TenantSummary | "recursiveMemberCount";
type TenantListRow = TenantSummary & { recursiveMemberCount: number }; type TenantListRow = TenantSummary & { recursiveMemberCount: number };
@@ -301,7 +288,7 @@ function TenantListPage() {
>({}); >({});
const [previewOpen, setPreviewOpen] = React.useState(false); const [previewOpen, setPreviewOpen] = React.useState(false);
const [selectedBulkStatus, setSelectedBulkStatus] = React.useState(""); const [selectedBulkStatus, setSelectedBulkStatus] = React.useState("");
const tenantTableScrollRef = React.useRef<HTMLDivElement | null>(null); const _tenantTableScrollRef = React.useRef<HTMLDivElement | null>(null);
const { data: profile } = useQuery({ const { data: profile } = useQuery({
queryKey: ["me"], queryKey: ["me"],

View File

@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { ArrowRight, Building2, Plus } from "lucide-react"; import { Building2, Plus } from "lucide-react";
import { Link, useNavigate, useParams } from "react-router-dom"; import { Link, useNavigate, useParams } from "react-router-dom";
import { import {
commonStickyTableHeaderClass, commonStickyTableHeaderClass,

View File

@@ -1,14 +1,6 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios"; import type { AxiosError } from "axios";
import { import { Loader2, Mail, Plus, User, UserPlus } from "lucide-react";
Loader2,
Mail,
MoreHorizontal,
Plus,
User,
UserMinus,
UserPlus,
} from "lucide-react";
import { Link, useNavigate, useParams } from "react-router-dom"; import { Link, useNavigate, useParams } from "react-router-dom";
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table"; import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
import { Badge } from "../../../components/ui/badge"; import { Badge } from "../../../components/ui/badge";
@@ -19,12 +11,6 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "../../../components/ui/card"; } from "../../../components/ui/card";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../../../components/ui/dropdown-menu";
import { import {
Table, Table,
TableBody, TableBody,
@@ -80,7 +66,7 @@ function TenantUsersPage() {
}, },
}); });
const handleRemoveMember = (userId: string, userName: string) => { const _handleRemoveMember = (userId: string, userName: string) => {
if (!tenantSlug) return; if (!tenantSlug) return;
if ( if (
window.confirm( window.confirm(

View File

@@ -1,6 +1,5 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Building2, Plus, Users } from "lucide-react"; import { Building2, Plus, Users } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table"; import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
import { Badge } from "../../../components/ui/badge"; import { Badge } from "../../../components/ui/badge";

View File

@@ -6,7 +6,6 @@ import {
Building2, Building2,
ChevronDown, ChevronDown,
ChevronRight, ChevronRight,
CornerDownRight,
Download, Download,
ExternalLink, ExternalLink,
FolderOpen, FolderOpen,
@@ -41,7 +40,6 @@ import {
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger,
} from "../../../components/ui/dialog"; } from "../../../components/ui/dialog";
import { import {
DropdownMenu, DropdownMenu,
@@ -52,7 +50,6 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "../../../components/ui/dropdown-menu"; } from "../../../components/ui/dropdown-menu";
import { Input } from "../../../components/ui/input"; import { Input } from "../../../components/ui/input";
import { Label } from "../../../components/ui/label";
import { ScrollArea } from "../../../components/ui/scroll-area"; import { ScrollArea } from "../../../components/ui/scroll-area";
import { import {
Table, Table,
@@ -62,15 +59,8 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "../../../components/ui/table"; } from "../../../components/ui/table";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "../../../components/ui/tabs";
import { toast } from "../../../components/ui/use-toast"; import { toast } from "../../../components/ui/use-toast";
import { import {
createUser,
exportTenantsCSV, exportTenantsCSV,
fetchAllTenants, fetchAllTenants,
fetchUsers, fetchUsers,
@@ -413,7 +403,7 @@ const MemberTable: React.FC<{
function TenantUserGroupsTab() { function TenantUserGroupsTab() {
const { tenantId } = useParams<{ tenantId: string }>(); const { tenantId } = useParams<{ tenantId: string }>();
const navigate = useNavigate(); const _navigate = useNavigate();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [selectedNodeId, setSelectedNodeId] = useState<string>(tenantId || ""); const [selectedNodeId, setSelectedNodeId] = useState<string>(tenantId || "");
@@ -855,7 +845,7 @@ const UserAddDialog: React.FC<{
try { try {
const res = await fetchUsers(20, 0, userSearch); const res = await fetchUsers(20, 0, userSearch);
setSearchResults(res.items); setSearchResults(res.items);
} catch (err) { } catch (_err) {
toast.error(t("msg.admin.users.list.fetch_error", "사용자 검색 실패")); toast.error(t("msg.admin.users.list.fetch_error", "사용자 검색 실패"));
} finally { } finally {
setIsSearching(false); setIsSearching(false);

View File

@@ -8,7 +8,6 @@ import {
Plus, Plus,
Save, Save,
Trash2, Trash2,
Mail,
X, X,
} from "lucide-react"; } from "lucide-react";
import * as React from "react"; import * as React from "react";
@@ -22,7 +21,6 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "../../components/ui/card"; } from "../../components/ui/card";
import { Checkbox } from "../../components/ui/checkbox";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -184,7 +182,7 @@ function UserCreatePage() {
if (e.key === "Enter" || e.key === "," || e.key === " ") { if (e.key === "Enter" || e.key === "," || e.key === " ") {
e.preventDefault(); e.preventDefault();
const value = newSubEmail.trim().replace(/,/g, ""); const value = newSubEmail.trim().replace(/,/g, "");
if (value && value.includes("@") && !currentSubEmails.includes(value)) { if (value?.includes("@") && !currentSubEmails.includes(value)) {
setValue("metadata.sub_email", [...currentSubEmails, value], { setValue("metadata.sub_email", [...currentSubEmails, value], {
shouldDirty: true, shouldDirty: true,
}); });
@@ -667,8 +665,7 @@ function UserCreatePage() {
onClick={() => { onClick={() => {
const value = newSubEmail.trim().replace(/,/g, ""); const value = newSubEmail.trim().replace(/,/g, "");
if ( if (
value && value?.includes("@") &&
value.includes("@") &&
!currentSubEmails.includes(value) !currentSubEmails.includes(value)
) { ) {
setValue( setValue(

View File

@@ -35,7 +35,6 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "../../components/ui/card"; } from "../../components/ui/card";
import { Checkbox } from "../../components/ui/checkbox";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -95,7 +94,10 @@ import { resolvePersonalTenant } from "./utils/personalTenant";
type UserFormValues = Omit<UserUpdateRequest, "metadata"> & { type UserFormValues = Omit<UserUpdateRequest, "metadata"> & {
email: string; email: string;
metadata: Record<string, Record<string, string | number | boolean | string[]> | string[] | undefined> & { metadata: Record<
string,
Record<string, string | number | boolean | string[]> | string[] | undefined
> & {
sub_email?: string[]; sub_email?: string[];
}; };
}; };
@@ -322,8 +324,8 @@ function UserDetailPage() {
const userId = params.id ?? ""; const userId = params.id ?? "";
const navigate = useNavigate(); const navigate = useNavigate();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [error, setError] = React.useState<string | null>(null); const [_error, _setError] = React.useState<string | null>(null);
const [successMsg, setSuccessMsg] = React.useState<string | null>(null); const [_successMsg, _setSuccessMsg] = React.useState<string | null>(null);
const [isPasswordResetOpen, setIsPasswordResetOpen] = React.useState(false); const [isPasswordResetOpen, setIsPasswordResetOpen] = React.useState(false);
const [generatedPassword, setGeneratedPassword] = React.useState< const [generatedPassword, setGeneratedPassword] = React.useState<
string | null string | null
@@ -419,7 +421,7 @@ function UserDetailPage() {
if (e.key === "Enter" || e.key === "," || e.key === " ") { if (e.key === "Enter" || e.key === "," || e.key === " ") {
e.preventDefault(); e.preventDefault();
const value = newSubEmail.trim().replace(/,/g, ""); const value = newSubEmail.trim().replace(/,/g, "");
if (value && value.includes("@") && !currentSubEmails.includes(value)) { if (value?.includes("@") && !currentSubEmails.includes(value)) {
setValue("metadata.sub_email", [...currentSubEmails, value], { setValue("metadata.sub_email", [...currentSubEmails, value], {
shouldDirty: true, shouldDirty: true,
}); });
@@ -595,7 +597,7 @@ function UserDetailPage() {
); );
}; };
const setPrimaryAppointment = (targetIndex: number) => { const _setPrimaryAppointment = (targetIndex: number) => {
setAdditionalAppointments((current) => setAdditionalAppointments((current) =>
current.map((appointment, index) => ({ current.map((appointment, index) => ({
...appointment, ...appointment,
@@ -793,11 +795,13 @@ function UserDetailPage() {
sub_email: rawSubEmail, sub_email: rawSubEmail,
...safeMetadata ...safeMetadata
} = cleanMetadata; } = cleanMetadata;
// Parse sub_email // Parse sub_email
let sub_email: string[] = []; let sub_email: string[] = [];
if (Array.isArray(rawSubEmail)) { if (Array.isArray(rawSubEmail)) {
sub_email = rawSubEmail.filter(e => typeof e === 'string' && e.includes("@")); sub_email = rawSubEmail.filter(
(e) => typeof e === "string" && e.includes("@"),
);
} else if (typeof rawSubEmail === "string" && rawSubEmail.trim() !== "") { } else if (typeof rawSubEmail === "string" && rawSubEmail.trim() !== "") {
sub_email = rawSubEmail sub_email = rawSubEmail
.split(/[;,\n\r\t]/) .split(/[;,\n\r\t]/)
@@ -816,7 +820,7 @@ function UserDetailPage() {
}; };
// email cannot be updated directly via this API in current backend implementation, // email cannot be updated directly via this API in current backend implementation,
// so we delete it from payload if it spread // so we delete it from payload if it spread
// @ts-ignore // @ts-expect-error
delete payload.email; delete payload.email;
payload.role = undefined; payload.role = undefined;
@@ -992,16 +996,21 @@ function UserDetailPage() {
<Mail size={14} className="text-primary/70" /> <Mail size={14} className="text-primary/70" />
{user.email} {user.email}
</div> </div>
{Boolean(user.metadata?.sub_email && {Boolean(
Array.isArray(user.metadata.sub_email) && user.metadata?.sub_email &&
user.metadata.sub_email.length > 0) && ( Array.isArray(user.metadata.sub_email) &&
<div className="flex items-center gap-1.5 bg-background px-2.5 py-1 rounded-full border"> user.metadata.sub_email.length > 0,
<Mail size={14} className="text-primary/40" /> ) && (
<span className="text-[10px] font-bold"> <div className="flex items-center gap-1.5 bg-background px-2.5 py-1 rounded-full border">
+{Array.isArray(user.metadata?.sub_email) ? user.metadata.sub_email.length : 0} <Mail size={14} className="text-primary/40" />
</span> <span className="text-[10px] font-bold">
</div> +
)} {Array.isArray(user.metadata?.sub_email)
? user.metadata.sub_email.length
: 0}
</span>
</div>
)}
{user.phone && ( {user.phone && (
<div className="flex items-center gap-1.5 bg-background px-2.5 py-1 rounded-full border"> <div className="flex items-center gap-1.5 bg-background px-2.5 py-1 rounded-full border">
<Shield size={14} className="text-primary/70" /> <Shield size={14} className="text-primary/70" />
@@ -1170,8 +1179,7 @@ function UserDetailPage() {
onClick={() => { onClick={() => {
const value = newSubEmail.trim().replace(/,/g, ""); const value = newSubEmail.trim().replace(/,/g, "");
if ( if (
value && value?.includes("@") &&
value.includes("@") &&
!currentSubEmails.includes(value) !currentSubEmails.includes(value)
) { ) {
setValue( setValue(

View File

@@ -1,5 +1,5 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom"; import { MemoryRouter } from "react-router-dom";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { createI18nMock } from "../../test/i18nMock"; import { createI18nMock } from "../../test/i18nMock";

View File

@@ -13,7 +13,6 @@ import {
ChevronDown, ChevronDown,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
Download,
FileDown, FileDown,
FileSpreadsheet, FileSpreadsheet,
LayoutDashboard, LayoutDashboard,
@@ -268,7 +267,7 @@ const UserListSearchControls = React.memo(function UserListSearchControls({
}); });
function UserListPage() { function UserListPage() {
const navigate = useNavigate(); const _navigate = useNavigate();
const [page, setPage] = React.useState(1); const [page, setPage] = React.useState(1);
const [search, setSearch] = React.useState(""); const [search, setSearch] = React.useState("");
const [selectedCompany, setSelectedCompany] = React.useState<string>(""); const [selectedCompany, setSelectedCompany] = React.useState<string>("");
@@ -563,7 +562,7 @@ function UserListPage() {
}, },
}); });
const handleApplyBulkStatus = () => { const _handleApplyBulkStatus = () => {
if (selectedUserIds.length === 0 || !selectedBulkStatus) return; if (selectedUserIds.length === 0 || !selectedBulkStatus) return;
bulkUpdateMutation.mutate({ bulkUpdateMutation.mutate({
userIds: selectedUserIds, userIds: selectedUserIds,
@@ -571,7 +570,7 @@ function UserListPage() {
}); });
}; };
const handleApplyBulkPermission = () => { const _handleApplyBulkPermission = () => {
if (selectedUserIds.length === 0 || !selectedBulkPermission) return; if (selectedUserIds.length === 0 || !selectedBulkPermission) return;
bulkUpdateMutation.mutate({ bulkUpdateMutation.mutate({
userIds: selectedUserIds, userIds: selectedUserIds,
@@ -594,7 +593,7 @@ function UserListPage() {
} }
}; };
const handleDelete = (userId: string, userName: string) => { const _handleDelete = (userId: string, userName: string) => {
if ( if (
!window.confirm( !window.confirm(
t( t(

View File

@@ -19,8 +19,6 @@ import {
bulkUpdateUsers, bulkUpdateUsers,
fetchAllTenants, fetchAllTenants,
fetchGroups, fetchGroups,
type GroupSummary,
type TenantSummary,
type UserSummary, type UserSummary,
} from "../../../lib/adminApi"; } from "../../../lib/adminApi";
import { t } from "../../../lib/i18n"; import { t } from "../../../lib/i18n";
@@ -49,7 +47,7 @@ export function UserBulkMoveGroupModal({
const [searchTerm, setSearchTerm] = React.useState(""); const [searchTerm, setSearchTerm] = React.useState("");
const [acknowledgeWarning, setAcknowledgeWarning] = React.useState(false); const [acknowledgeWarning, setAcknowledgeWarning] = React.useState(false);
const queryClient = useQueryClient(); const _queryClient = useQueryClient();
const { data: tenantsData } = useQuery({ const { data: tenantsData } = useQuery({
queryKey: ["tenants", "all"], queryKey: ["tenants", "all"],

View File

@@ -102,7 +102,7 @@ export function isHanmacFamilyTenant<T extends TenantFilterTarget>(
tenants: T[], tenants: T[],
hanmacFamilyTenantId?: string, hanmacFamilyTenantId?: string,
) { ) {
if (!tenant || !tenant.id) return false; if (!tenant?.id) return false;
const rootTenantId = resolveHanmacFamilyTenantId( const rootTenantId = resolveHanmacFamilyTenantId(
tenants, tenants,

View File

@@ -24,7 +24,7 @@ export function buildTenantFullTree(
}); });
} }
const visitedDuringBuild = new Set<string>(); const _visitedDuringBuild = new Set<string>();
// Build initial children relations and prevent simple cycles // Build initial children relations and prevent simple cycles
for (const t of allTenants) { for (const t of allTenants) {
if (t.parentId && t.parentId !== t.id) { if (t.parentId && t.parentId !== t.id) {

View File

@@ -258,7 +258,7 @@ test.describe("Tenants Management", () => {
page, page,
}) => { }) => {
await page.setViewportSize({ width: 900, height: 700 }); await page.setViewportSize({ width: 900, height: 700 });
let requestCount = 0; let _requestCount = 0;
await page.route("**/api/v1/admin/tenants**", async (route) => { await page.route("**/api/v1/admin/tenants**", async (route) => {
if (route.request().method() !== "GET") { if (route.request().method() !== "GET") {
@@ -266,7 +266,7 @@ test.describe("Tenants Management", () => {
} }
const url = new URL(route.request().url()); const url = new URL(route.request().url());
const cursor = url.searchParams.get("cursor"); const cursor = url.searchParams.get("cursor");
requestCount += 1; _requestCount += 1;
if (!cursor) { if (!cursor) {
return route.fulfill({ return route.fulfill({

View File

@@ -5,7 +5,9 @@ test.describe("Users Bulk Upload Secondary Emails", () => {
await page.addInitScript(() => { await page.addInitScript(() => {
window.localStorage.setItem("locale", "ko"); window.localStorage.setItem("locale", "ko");
window.localStorage.setItem("admin_session", "fake-token"); window.localStorage.setItem("admin_session", "fake-token");
(window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean })._IS_TEST_MODE = true; (
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
)._IS_TEST_MODE = true;
const authData = { const authData = {
access_token: "fake-token", access_token: "fake-token",
@@ -13,12 +15,20 @@ test.describe("Users Bulk Upload Secondary Emails", () => {
profile: { sub: "admin-user", name: "Admin", role: "super_admin" }, profile: { sub: "admin-user", name: "Admin", role: "super_admin" },
expires_at: Math.floor(Date.now() / 1000) + 36000, expires_at: Math.floor(Date.now() / 1000) + 36000,
}; };
window.localStorage.setItem("oidc.user:http://localhost:5000/oidc:adminfront", JSON.stringify(authData)); window.localStorage.setItem(
"oidc.user:http://localhost:5000/oidc:adminfront",
JSON.stringify(authData),
);
}); });
await page.route("**/api/v1/user/me", async (route) => { await page.route("**/api/v1/user/me", async (route) => {
return route.fulfill({ return route.fulfill({
json: { id: "admin-user", name: "Admin", role: "super_admin", manageableTenants: [] }, json: {
id: "admin-user",
name: "Admin",
role: "super_admin",
manageableTenants: [],
},
headers: { "Access-Control-Allow-Origin": "*" }, headers: { "Access-Control-Allow-Origin": "*" },
}); });
}); });
@@ -31,7 +41,7 @@ test.describe("Users Bulk Upload Secondary Emails", () => {
}); });
await page.route("**/api/v1/admin/users*", async (route) => { await page.route("**/api/v1/admin/users*", async (route) => {
if(route.request().url().includes("/bulk")) { if (route.request().url().includes("/bulk")) {
return route.continue(); return route.continue();
} }
return route.fulfill({ return route.fulfill({
@@ -45,14 +55,20 @@ test.describe("Users Bulk Upload Secondary Emails", () => {
}); });
}); });
test("should parse secondary_emails and send to backend", async ({ page }) => { test("should parse secondary_emails and send to backend", async ({
page,
}) => {
let bulkPayload: any = null; let bulkPayload: any = null;
await page.route("**/api/v1/admin/users/bulk", async (route) => { await page.route("**/api/v1/admin/users/bulk", async (route) => {
if (route.request().method() === "POST") { if (route.request().method() === "POST") {
bulkPayload = route.request().postDataJSON(); bulkPayload = route.request().postDataJSON();
return route.fulfill({ return route.fulfill({
json: { results: [{ email: "test@example.com", success: true, userId: "u-1" }] }, json: {
results: [
{ email: "test@example.com", success: true, userId: "u-1" },
],
},
headers: { "Access-Control-Allow-Origin": "*" }, headers: { "Access-Control-Allow-Origin": "*" },
}); });
} }
@@ -60,21 +76,26 @@ test.describe("Users Bulk Upload Secondary Emails", () => {
}); });
await page.goto("/users"); await page.goto("/users");
await expect(page.getByTestId("page-title")).toContainText(/사용자|Users/i, { timeout: 20000 }); await expect(page.getByTestId("page-title")).toContainText(
/사용자|Users/i,
{ timeout: 20000 },
);
await page.getByTestId("user-data-mgmt-btn").click(); await page.getByTestId("user-data-mgmt-btn").click();
await page.getByRole("menuitem", { name: /일괄 임포트|일괄 등록|Bulk Import/i }).click(); await page
.getByRole("menuitem", { name: /일괄 임포트|일괄 등록|Bulk Import/i })
.click();
// Create a mock CSV with secondary_emails // Create a mock CSV with secondary_emails
const csvContent = `email,sub_email,name,phone,role,tenant_slug\ntest@example.com,sub1@test.com;sub2@test.com,홍길동,010-1234-5678,user,tenant-slug`; const csvContent = `email,sub_email,name,phone,role,tenant_slug\ntest@example.com,sub1@test.com;sub2@test.com,홍길동,010-1234-5678,user,tenant-slug`;
const fileChooserPromise = page.waitForEvent('filechooser'); const fileChooserPromise = page.waitForEvent("filechooser");
await page.getByText(/파일 선택|Change file|Select file/i).click(); await page.getByText(/파일 선택|Change file|Select file/i).click();
const fileChooser = await fileChooserPromise; const fileChooser = await fileChooserPromise;
await fileChooser.setFiles({ await fileChooser.setFiles({
name: 'users_with_secondary.csv', name: "users_with_secondary.csv",
mimeType: 'text/csv', mimeType: "text/csv",
buffer: Buffer.from(csvContent), buffer: Buffer.from(csvContent),
}); });
@@ -87,9 +108,9 @@ test.describe("Users Bulk Upload Secondary Emails", () => {
expect(bulkPayload).not.toBeNull(); expect(bulkPayload).not.toBeNull();
expect(bulkPayload.users).toHaveLength(1); expect(bulkPayload.users).toHaveLength(1);
// The most important check - does it parse to the metadata // The most important check - does it parse to the metadata
expect(bulkPayload.users[0].metadata.sub_email).toContain("sub1@test.com"); expect(bulkPayload.users[0].metadata.sub_email).toContain("sub1@test.com");
expect(bulkPayload.users[0].metadata.sub_email).toContain("sub2@test.com"); expect(bulkPayload.users[0].metadata.sub_email).toContain("sub2@test.com");
}); });
}); });