2025-11-05 16:36:32 +09:00
|
|
|
import {
|
|
|
|
|
ResizableDialog,
|
|
|
|
|
ResizableDialogContent,
|
|
|
|
|
ResizableDialogHeader,
|
|
|
|
|
ResizableDialogTitle,
|
|
|
|
|
ResizableDialogDescription,
|
|
|
|
|
ResizableDialogFooter,
|
|
|
|
|
} from "@/components/ui/resizable-dialog";
|
2025-08-21 09:41:46 +09:00
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
|
|
|
import { Camera, X } from "lucide-react";
|
|
|
|
|
import { ProfileFormData } from "@/types/profile";
|
|
|
|
|
|
|
|
|
|
// 알림 모달 컴포넌트
|
|
|
|
|
interface AlertModalProps {
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
title: string;
|
|
|
|
|
message: string;
|
|
|
|
|
type?: "success" | "error" | "info";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function AlertModal({ isOpen, onClose, title, message, type = "info" }: AlertModalProps) {
|
|
|
|
|
const getTypeColor = () => {
|
|
|
|
|
switch (type) {
|
|
|
|
|
case "success":
|
|
|
|
|
return "text-green-600";
|
|
|
|
|
case "error":
|
2025-10-02 14:34:15 +09:00
|
|
|
return "text-destructive";
|
2025-08-21 09:41:46 +09:00
|
|
|
default:
|
2025-10-02 14:34:15 +09:00
|
|
|
return "text-primary";
|
2025-08-21 09:41:46 +09:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2025-11-05 16:36:32 +09:00
|
|
|
<ResizableDialog open={isOpen} onOpenChange={onClose}>
|
|
|
|
|
<ResizableDialogContent className="sm:max-w-md">
|
|
|
|
|
<ResizableDialogHeader>
|
|
|
|
|
<ResizableDialogTitle className={getTypeColor()}>{title}</ResizableDialogTitle>
|
|
|
|
|
</ResizableDialogHeader>
|
2025-08-21 09:41:46 +09:00
|
|
|
<div className="py-4">
|
2025-10-02 14:34:15 +09:00
|
|
|
<p className="text-sm text-muted-foreground">{message}</p>
|
2025-08-21 09:41:46 +09:00
|
|
|
</div>
|
|
|
|
|
<div className="flex justify-end">
|
|
|
|
|
<Button onClick={onClose} className="w-20">
|
|
|
|
|
확인
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-11-05 16:36:32 +09:00
|
|
|
</ResizableDialogContent>
|
|
|
|
|
</ResizableDialog>
|
2025-08-21 09:41:46 +09:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ProfileModalProps {
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
user: any;
|
|
|
|
|
formData: ProfileFormData;
|
|
|
|
|
selectedImage: string;
|
|
|
|
|
isSaving: boolean;
|
2025-08-28 10:05:06 +09:00
|
|
|
departments: Array<{
|
|
|
|
|
deptCode: string;
|
|
|
|
|
deptName: string;
|
|
|
|
|
}>;
|
2025-08-21 09:41:46 +09:00
|
|
|
alertModal: {
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
title: string;
|
|
|
|
|
message: string;
|
|
|
|
|
type: "success" | "error" | "info";
|
|
|
|
|
};
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
onFormChange: (field: keyof ProfileFormData, value: string) => void;
|
|
|
|
|
onImageSelect: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
|
|
|
onImageRemove: () => void;
|
|
|
|
|
onSave: () => void;
|
|
|
|
|
onAlertClose: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 프로필 수정 모달 컴포넌트
|
|
|
|
|
*/
|
|
|
|
|
export function ProfileModal({
|
|
|
|
|
isOpen,
|
|
|
|
|
user,
|
|
|
|
|
formData,
|
|
|
|
|
selectedImage,
|
|
|
|
|
isSaving,
|
2025-08-28 10:05:06 +09:00
|
|
|
departments,
|
2025-08-21 09:41:46 +09:00
|
|
|
alertModal,
|
|
|
|
|
onClose,
|
|
|
|
|
onFormChange,
|
|
|
|
|
onImageSelect,
|
|
|
|
|
onImageRemove,
|
|
|
|
|
onSave,
|
|
|
|
|
onAlertClose,
|
|
|
|
|
}: ProfileModalProps) {
|
|
|
|
|
return (
|
|
|
|
|
<>
|
2025-11-05 16:36:32 +09:00
|
|
|
<ResizableDialog open={isOpen} onOpenChange={onClose}>
|
|
|
|
|
<ResizableDialogContent className="sm:max-w-[500px]">
|
|
|
|
|
<ResizableDialogHeader>
|
|
|
|
|
<ResizableDialogTitle>프로필 수정</ResizableDialogTitle>
|
|
|
|
|
</ResizableDialogHeader>
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
<div className="grid gap-6 py-4">
|
|
|
|
|
{/* 프로필 사진 섹션 */}
|
|
|
|
|
<div className="flex flex-col items-center space-y-4">
|
|
|
|
|
<div className="relative">
|
2025-09-30 15:45:21 +09:00
|
|
|
<div className="relative flex h-24 w-24 shrink-0 overflow-hidden rounded-full">
|
|
|
|
|
{selectedImage && selectedImage.trim() !== "" ? (
|
|
|
|
|
<img
|
|
|
|
|
src={selectedImage}
|
|
|
|
|
alt="프로필 사진 미리보기"
|
|
|
|
|
className="aspect-square h-full w-full object-cover"
|
|
|
|
|
/>
|
2025-08-21 09:41:46 +09:00
|
|
|
) : (
|
2025-09-30 15:45:21 +09:00
|
|
|
<div className="flex h-full w-full items-center justify-center rounded-full bg-slate-200 text-2xl font-semibold text-slate-700">
|
|
|
|
|
{formData.userName?.substring(0, 1)?.toUpperCase() || "U"}
|
|
|
|
|
</div>
|
2025-08-21 09:41:46 +09:00
|
|
|
)}
|
2025-09-30 15:45:21 +09:00
|
|
|
</div>
|
2025-08-21 09:41:46 +09:00
|
|
|
|
2025-09-30 15:45:21 +09:00
|
|
|
{selectedImage && selectedImage.trim() !== "" ? (
|
2025-08-21 09:41:46 +09:00
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="destructive"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="absolute -top-2 -right-2 h-6 w-6 rounded-full"
|
|
|
|
|
onClick={onImageRemove}
|
|
|
|
|
>
|
|
|
|
|
<X className="h-3 w-3" />
|
|
|
|
|
</Button>
|
2025-09-30 15:45:21 +09:00
|
|
|
) : null}
|
2025-08-21 09:41:46 +09:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => document.getElementById("profile-image-input")?.click()}
|
|
|
|
|
className="flex items-center gap-2"
|
|
|
|
|
>
|
|
|
|
|
<Camera className="h-4 w-4" />
|
|
|
|
|
사진 선택
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
id="profile-image-input"
|
|
|
|
|
type="file"
|
|
|
|
|
accept="image/*"
|
|
|
|
|
onChange={onImageSelect}
|
|
|
|
|
className="hidden"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 사용자 정보 폼 */}
|
|
|
|
|
<div className="grid gap-4">
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="userId">사용자 ID</Label>
|
|
|
|
|
<Input id="userId" value={user?.userId || ""} disabled className="bg-muted" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="userName">이름</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="userName"
|
|
|
|
|
value={formData.userName}
|
|
|
|
|
onChange={(e) => onFormChange("userName", e.target.value)}
|
|
|
|
|
placeholder="이름을 입력하세요"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="email">이메일</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="email"
|
|
|
|
|
type="email"
|
|
|
|
|
value={formData.email}
|
|
|
|
|
onChange={(e) => onFormChange("email", e.target.value)}
|
|
|
|
|
placeholder="이메일을 입력하세요"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="deptName">부서</Label>
|
2025-08-28 10:05:06 +09:00
|
|
|
<Select value={formData.deptName} onValueChange={(value) => onFormChange("deptName", value)}>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue placeholder="부서 선택" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{Array.isArray(departments) && departments.length > 0 ? (
|
|
|
|
|
departments.map((department) => (
|
|
|
|
|
<SelectItem key={department.deptCode} value={department.deptName}>
|
|
|
|
|
{department.deptName}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))
|
|
|
|
|
) : (
|
|
|
|
|
<SelectItem value="no-data" disabled>
|
|
|
|
|
부서 정보가 없습니다
|
|
|
|
|
</SelectItem>
|
|
|
|
|
)}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2025-08-21 09:41:46 +09:00
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="positionName">직급</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="positionName"
|
|
|
|
|
value={formData.positionName}
|
|
|
|
|
onChange={(e) => onFormChange("positionName", e.target.value)}
|
|
|
|
|
placeholder="직급을 입력하세요"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="locale">지역</Label>
|
|
|
|
|
<Select value={formData.locale || ""} onValueChange={(value) => onFormChange("locale", value)}>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue placeholder="선택해주세요" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="KR">한국어 (KR)</SelectItem>
|
|
|
|
|
<SelectItem value="US">English (US)</SelectItem>
|
|
|
|
|
<SelectItem value="JP">日本語 (JP)</SelectItem>
|
|
|
|
|
<SelectItem value="CN">中文 (CN)</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-11-05 16:36:32 +09:00
|
|
|
<ResizableDialogFooter>
|
2025-08-21 09:41:46 +09:00
|
|
|
<Button type="button" variant="outline" onClick={onClose} disabled={isSaving}>
|
|
|
|
|
취소
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="button" onClick={onSave} disabled={isSaving}>
|
|
|
|
|
{isSaving ? "저장 중..." : "저장"}
|
|
|
|
|
</Button>
|
2025-11-05 16:36:32 +09:00
|
|
|
</ResizableDialogFooter>
|
|
|
|
|
</ResizableDialogContent>
|
|
|
|
|
</ResizableDialog>
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
{/* 알림 모달 */}
|
|
|
|
|
<AlertModal
|
|
|
|
|
isOpen={alertModal.isOpen}
|
|
|
|
|
onClose={onAlertClose}
|
|
|
|
|
title={alertModal.title}
|
|
|
|
|
message={alertModal.message}
|
|
|
|
|
type={alertModal.type}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|