From 4176fed07f481b5cdf435deacd04a8035c8d244f Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Mon, 9 Mar 2026 13:21:18 +0900 Subject: [PATCH] =?UTF-8?q?feat(pop):=20pop-profile=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80=20-=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=ED=94=84=EB=A1=9C=ED=95=84/PC=EC=A0=84?= =?UTF-8?q?=ED=99=98/=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B4=EB=84=88=20=ED=8C=94=EB=A0=88=ED=8A=B8?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=B0=B0=EC=B9=98=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=9C=2010=EB=B2=88=EC=A7=B8=20POP=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EB=A1=9C=20pop-profile=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4.=20=ED=99=94=EB=A9=B4=20=EC=84=A4?= =?UTF-8?q?=EA=B3=84=EC=9E=90=EA=B0=80=20=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=97=90=EB=A7=8C=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EB=B0=B0=EC=B9=98?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20UI=EA=B0=80=20=EC=95=84=EB=8B=8C=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.=20[=EB=B7=B0=EC=96=B4=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8]=20-=20useAuth()=20=EC=97=B0=EB=8F=99?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=8B=A4=EC=A0=9C=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20-=20Popover=20=EB=93=9C=EB=A1=AD=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20+=20POP=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20/=20PC=20?= =?UTF-8?q?=EB=AA=A8=EB=93=9C=20/=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83?= =?UTF-8?q?=20-=20=EB=B9=84=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B2=84=ED=8A=BC=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20-=20=EC=95=84=EB=B0=94=ED=83=80:=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=82=AC=EC=A7=84=20=EB=98=90=EB=8A=94=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=9D=B4=EB=8B=88=EC=85=9C=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20[=EC=84=A4=EC=A0=95=20=ED=8C=A8=EB=84=90]=20-=20?= =?UTF-8?q?=EC=95=84=EB=B0=94=ED=83=80=20=ED=81=AC=EA=B8=B0=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20(sm=2032px=20/=20md=2040px=20/=20lg=2048px)=20-=20?= =?UTF-8?q?=EB=A9=94=EB=89=B4=20=ED=95=AD=EB=AA=A9=20=EA=B0=9C=EB=B3=84=20?= =?UTF-8?q?on/off=20(=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20/=20PC=20=EB=AA=A8=EB=93=9C=20=EC=A0=84=ED=99=98=20?= =?UTF-8?q?/=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83)=20[=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B4=EB=84=88=20=ED=86=B5=ED=95=A9]=20-=20PopComponentType?= =?UTF-8?q?=EC=97=90=20"pop-profile"=20=EC=B6=94=EA=B0=80=20-=20DEFAULT=5F?= =?UTF-8?q?COMPONENT=5FGRID=5FSIZE:=201x1=20-=20PALETTE=5FITEMS:=20UserCir?= =?UTF-8?q?cle=20=EC=95=84=EC=9D=B4=EC=BD=98=20+=20=EC=84=A4=EB=AA=85=20-?= =?UTF-8?q?=20COMPONENT=5FTYPE=5FLABELS:=20"=ED=94=84=EB=A1=9C=ED=95=84"?= =?UTF-8?q?=20-=20PopComponentRegistry=20=EB=93=B1=EB=A1=9D=20(category:?= =?UTF-8?q?=20action)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pop/designer/panels/ComponentPalette.tsx | 8 +- .../pop/designer/renderers/PopRenderer.tsx | 1 + .../pop/designer/types/pop-layout.ts | 3 +- frontend/lib/registry/pop-components/index.ts | 4 +- .../registry/pop-components/pop-profile.tsx | 336 ++++++++++++++++++ 5 files changed, 347 insertions(+), 5 deletions(-) create mode 100644 frontend/lib/registry/pop-components/pop-profile.tsx diff --git a/frontend/components/pop/designer/panels/ComponentPalette.tsx b/frontend/components/pop/designer/panels/ComponentPalette.tsx index a09eb44d..471db3fd 100644 --- a/frontend/components/pop/designer/panels/ComponentPalette.tsx +++ b/frontend/components/pop/designer/panels/ComponentPalette.tsx @@ -3,7 +3,7 @@ import { useDrag } from "react-dnd"; import { cn } from "@/lib/utils"; import { PopComponentType } from "../types/pop-layout"; -import { Square, FileText, MousePointer, BarChart3, LayoutGrid, MousePointerClick, List, Search, TextCursorInput, ScanLine } from "lucide-react"; +import { Square, FileText, MousePointer, BarChart3, LayoutGrid, MousePointerClick, List, Search, TextCursorInput, ScanLine, UserCircle } from "lucide-react"; import { DND_ITEM_TYPES } from "../constants"; // 컴포넌트 정의 @@ -75,6 +75,12 @@ const PALETTE_ITEMS: PaletteItem[] = [ icon: ScanLine, description: "바코드/QR 카메라 스캔", }, + { + type: "pop-profile", + label: "프로필", + icon: UserCircle, + description: "사용자 프로필 / PC 전환 / 로그아웃", + }, ]; // 드래그 가능한 컴포넌트 아이템 diff --git a/frontend/components/pop/designer/renderers/PopRenderer.tsx b/frontend/components/pop/designer/renderers/PopRenderer.tsx index 32ac610b..0fb99fc5 100644 --- a/frontend/components/pop/designer/renderers/PopRenderer.tsx +++ b/frontend/components/pop/designer/renderers/PopRenderer.tsx @@ -76,6 +76,7 @@ const COMPONENT_TYPE_LABELS: Record = { "pop-string-list": "리스트 목록", "pop-search": "검색", "pop-field": "입력", + "pop-profile": "프로필", }; // ======================================== diff --git a/frontend/components/pop/designer/types/pop-layout.ts b/frontend/components/pop/designer/types/pop-layout.ts index 2e400504..f88b6d59 100644 --- a/frontend/components/pop/designer/types/pop-layout.ts +++ b/frontend/components/pop/designer/types/pop-layout.ts @@ -9,7 +9,7 @@ /** * POP 컴포넌트 타입 */ -export type PopComponentType = "pop-sample" | "pop-text" | "pop-icon" | "pop-dashboard" | "pop-card-list" | "pop-button" | "pop-string-list" | "pop-search" | "pop-field" | "pop-scanner"; +export type PopComponentType = "pop-sample" | "pop-text" | "pop-icon" | "pop-dashboard" | "pop-card-list" | "pop-button" | "pop-string-list" | "pop-search" | "pop-field" | "pop-scanner" | "pop-profile"; /** * 데이터 흐름 정의 @@ -363,6 +363,7 @@ export const DEFAULT_COMPONENT_GRID_SIZE: Record = { + 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 = { + 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 ( +
+ + + + + + {isLoggedIn && user ? ( + <> + {/* 사용자 정보 */} +
+
+ {user.photo && user.photo.trim() !== "" && user.photo !== "null" ? ( + {user.userName + ) : ( + initial + )} +
+
+ + {user.userName || "사용자"} ({user.userId || ""}) + + + {user.deptName || "부서 정보 없음"} + +
+
+ + {/* 메뉴 항목 */} +
+ {config.showDashboardLink && ( + + )} + {config.showPcMode && ( + + )} + {config.showLogout && ( + <> +
+ + + )} +
+ + ) : ( +
+

+ 로그인이 필요합니다 +

+ +
+ )} + + +
+ ); +} + +// ======================================== +// 설정 패널 +// ======================================== + +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) => { + onUpdate({ ...config, ...partial }); + }; + + return ( +
+ {/* 아바타 크기 */} +
+ + +
+ + {/* 메뉴 항목 토글 */} +
+ + +
+ + updateConfig({ showDashboardLink: v })} + /> +
+ +
+ + updateConfig({ showPcMode: v })} + /> +
+ +
+ + updateConfig({ showLogout: v })} + /> +
+
+
+ ); +} + +// ======================================== +// 디자이너 미리보기 +// ======================================== + +function PopProfilePreview({ config }: { config?: PopProfileConfig }) { + const size = AVATAR_SIZE_MAP[config?.avatarSize || "md"]; + return ( +
+
+ +
+ 프로필 +
+ ); +} + +// ======================================== +// 레지스트리 등록 +// ======================================== + +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"], +});