337 lines
11 KiB
TypeScript
337 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useMemo } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { cn } from "@/lib/utils";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
import { Monitor, LayoutGrid, LogOut, UserCircle } from "lucide-react";
|
|
import { PopComponentRegistry } from "@/lib/registry/PopComponentRegistry";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
|
|
// ========================================
|
|
// 타입 정의
|
|
// ========================================
|
|
|
|
type AvatarSize = "sm" | "md" | "lg";
|
|
|
|
export interface PopProfileConfig {
|
|
avatarSize?: AvatarSize;
|
|
showDashboardLink?: boolean;
|
|
showPcMode?: boolean;
|
|
showLogout?: boolean;
|
|
}
|
|
|
|
const DEFAULT_CONFIG: PopProfileConfig = {
|
|
avatarSize: "md",
|
|
showDashboardLink: true,
|
|
showPcMode: true,
|
|
showLogout: true,
|
|
};
|
|
|
|
const AVATAR_SIZE_MAP: Record<AvatarSize, { container: string; text: string; px: number }> = {
|
|
sm: { container: "h-8 w-8", text: "text-sm", px: 32 },
|
|
md: { container: "h-10 w-10", text: "text-base", px: 40 },
|
|
lg: { container: "h-12 w-12", text: "text-lg", px: 48 },
|
|
};
|
|
|
|
const AVATAR_SIZE_LABELS: Record<AvatarSize, string> = {
|
|
sm: "작은 (32px)",
|
|
md: "보통 (40px)",
|
|
lg: "큰 (48px)",
|
|
};
|
|
|
|
// ========================================
|
|
// 뷰어 컴포넌트
|
|
// ========================================
|
|
|
|
interface PopProfileComponentProps {
|
|
config?: PopProfileConfig;
|
|
componentId?: string;
|
|
screenId?: string;
|
|
}
|
|
|
|
function PopProfileComponent({ config: rawConfig }: PopProfileComponentProps) {
|
|
const router = useRouter();
|
|
const { user, isLoggedIn, logout } = useAuth();
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const config = useMemo(() => ({
|
|
...DEFAULT_CONFIG,
|
|
...rawConfig,
|
|
}), [rawConfig]);
|
|
|
|
const sizeInfo = AVATAR_SIZE_MAP[config.avatarSize || "md"];
|
|
const initial = user?.userName?.substring(0, 1)?.toUpperCase() || "?";
|
|
|
|
const handlePcMode = () => {
|
|
setOpen(false);
|
|
router.push("/");
|
|
};
|
|
|
|
const handleDashboard = () => {
|
|
setOpen(false);
|
|
router.push("/pop");
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
setOpen(false);
|
|
await logout();
|
|
};
|
|
|
|
const handleLogin = () => {
|
|
setOpen(false);
|
|
router.push("/login");
|
|
};
|
|
|
|
return (
|
|
<div className="flex h-full w-full items-center justify-center">
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<button
|
|
className={cn(
|
|
"flex items-center justify-center rounded-full",
|
|
"bg-primary text-primary-foreground font-bold",
|
|
"border-2 border-primary/20 cursor-pointer",
|
|
"transition-all duration-150",
|
|
"hover:scale-105 hover:border-primary/40",
|
|
"active:scale-95",
|
|
sizeInfo.container,
|
|
sizeInfo.text,
|
|
)}
|
|
style={{ minWidth: sizeInfo.px, minHeight: sizeInfo.px }}
|
|
>
|
|
{user?.photo && user.photo.trim() !== "" && user.photo !== "null" ? (
|
|
<img
|
|
src={user.photo}
|
|
alt={user.userName || "User"}
|
|
className="h-full w-full rounded-full object-cover"
|
|
/>
|
|
) : (
|
|
initial
|
|
)}
|
|
</button>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
className="w-60 p-0"
|
|
align="end"
|
|
sideOffset={8}
|
|
>
|
|
{isLoggedIn && user ? (
|
|
<>
|
|
{/* 사용자 정보 */}
|
|
<div className="flex items-center gap-3 border-b p-4">
|
|
<div className={cn(
|
|
"flex shrink-0 items-center justify-center rounded-full",
|
|
"bg-primary text-primary-foreground font-bold",
|
|
"h-10 w-10 text-base",
|
|
)}>
|
|
{user.photo && user.photo.trim() !== "" && user.photo !== "null" ? (
|
|
<img
|
|
src={user.photo}
|
|
alt={user.userName || "User"}
|
|
className="h-full w-full rounded-full object-cover"
|
|
/>
|
|
) : (
|
|
initial
|
|
)}
|
|
</div>
|
|
<div className="flex min-w-0 flex-col gap-0.5">
|
|
<span className="truncate text-sm font-semibold">
|
|
{user.userName || "사용자"} ({user.userId || ""})
|
|
</span>
|
|
<span className="truncate text-xs text-muted-foreground">
|
|
{user.deptName || "부서 정보 없음"}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 메뉴 항목 */}
|
|
<div className="p-1.5">
|
|
{config.showDashboardLink && (
|
|
<button
|
|
onClick={handleDashboard}
|
|
className="flex w-full items-center gap-3 rounded-md px-3 py-3 text-sm transition-colors hover:bg-accent"
|
|
style={{ minHeight: 48 }}
|
|
>
|
|
<LayoutGrid className="h-4 w-4 text-muted-foreground" />
|
|
POP 대시보드
|
|
</button>
|
|
)}
|
|
{config.showPcMode && (
|
|
<button
|
|
onClick={handlePcMode}
|
|
className="flex w-full items-center gap-3 rounded-md px-3 py-3 text-sm transition-colors hover:bg-accent"
|
|
style={{ minHeight: 48 }}
|
|
>
|
|
<Monitor className="h-4 w-4 text-muted-foreground" />
|
|
PC 모드
|
|
</button>
|
|
)}
|
|
{config.showLogout && (
|
|
<>
|
|
<div className="mx-2 my-1 border-t" />
|
|
<button
|
|
onClick={handleLogout}
|
|
className="flex w-full items-center gap-3 rounded-md px-3 py-3 text-sm text-destructive transition-colors hover:bg-destructive/10"
|
|
style={{ minHeight: 48 }}
|
|
>
|
|
<LogOut className="h-4 w-4" />
|
|
로그아웃
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="p-4">
|
|
<p className="mb-3 text-center text-sm text-muted-foreground">
|
|
로그인이 필요합니다
|
|
</p>
|
|
<button
|
|
onClick={handleLogin}
|
|
className="flex w-full items-center justify-center gap-2 rounded-md bg-primary px-3 py-3 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
|
style={{ minHeight: 48 }}
|
|
>
|
|
로그인
|
|
</button>
|
|
</div>
|
|
)}
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ========================================
|
|
// 설정 패널
|
|
// ========================================
|
|
|
|
interface PopProfileConfigPanelProps {
|
|
config: PopProfileConfig;
|
|
onUpdate: (config: PopProfileConfig) => void;
|
|
}
|
|
|
|
function PopProfileConfigPanel({ config: rawConfig, onUpdate }: PopProfileConfigPanelProps) {
|
|
const config = useMemo(() => ({
|
|
...DEFAULT_CONFIG,
|
|
...rawConfig,
|
|
}), [rawConfig]);
|
|
|
|
const updateConfig = (partial: Partial<PopProfileConfig>) => {
|
|
onUpdate({ ...config, ...partial });
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4 p-3">
|
|
{/* 아바타 크기 */}
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs sm:text-sm">아바타 크기</Label>
|
|
<Select
|
|
value={config.avatarSize || "md"}
|
|
onValueChange={(v) => updateConfig({ avatarSize: v as AvatarSize })}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{(Object.entries(AVATAR_SIZE_LABELS) as [AvatarSize, string][]).map(([key, label]) => (
|
|
<SelectItem key={key} value={key} className="text-xs sm:text-sm">
|
|
{label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 메뉴 항목 토글 */}
|
|
<div className="space-y-3">
|
|
<Label className="text-xs sm:text-sm">메뉴 항목</Label>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs text-muted-foreground">POP 대시보드 이동</Label>
|
|
<Switch
|
|
checked={config.showDashboardLink ?? true}
|
|
onCheckedChange={(v) => updateConfig({ showDashboardLink: v })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs text-muted-foreground">PC 모드 전환</Label>
|
|
<Switch
|
|
checked={config.showPcMode ?? true}
|
|
onCheckedChange={(v) => updateConfig({ showPcMode: v })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs text-muted-foreground">로그아웃</Label>
|
|
<Switch
|
|
checked={config.showLogout ?? true}
|
|
onCheckedChange={(v) => updateConfig({ showLogout: v })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ========================================
|
|
// 디자이너 미리보기
|
|
// ========================================
|
|
|
|
function PopProfilePreview({ config }: { config?: PopProfileConfig }) {
|
|
const size = AVATAR_SIZE_MAP[config?.avatarSize || "md"];
|
|
return (
|
|
<div className="flex h-full w-full items-center justify-center gap-2">
|
|
<div className={cn(
|
|
"flex items-center justify-center rounded-full",
|
|
"bg-primary/20 text-primary",
|
|
size.container, size.text,
|
|
)}>
|
|
<UserCircle className="h-5 w-5" />
|
|
</div>
|
|
<span className="text-xs text-muted-foreground">프로필</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ========================================
|
|
// 레지스트리 등록
|
|
// ========================================
|
|
|
|
PopComponentRegistry.registerComponent({
|
|
id: "pop-profile",
|
|
name: "프로필",
|
|
description: "사용자 프로필 / PC 전환 / 로그아웃",
|
|
category: "action",
|
|
icon: "UserCircle",
|
|
component: PopProfileComponent,
|
|
configPanel: PopProfileConfigPanel,
|
|
preview: PopProfilePreview,
|
|
defaultProps: {
|
|
avatarSize: "md",
|
|
showDashboardLink: true,
|
|
showPcMode: true,
|
|
showLogout: true,
|
|
},
|
|
connectionMeta: {
|
|
sendable: [],
|
|
receivable: [],
|
|
},
|
|
touchOptimized: true,
|
|
supportedDevices: ["mobile", "tablet"],
|
|
});
|