Files
invyone/frontend/components/layout/ProfileModal.tsx
T
Johngreen 2b954db854
Build & Deploy to K8s / build-and-deploy (push) Failing after 6m29s
프로필 모달에서 차량/운전자 정보 섹션 제거
ProfileModal 의 isDriver 토글 섹션 + 새 차량 등록 모달 + DriverInfo
/DriverFormData 타입 + 관련 props 모두 제거. 호출부(AppLayout) 와
useProfile 훅의 driver/vehicle 상태 관리 로직도 함께 정리.

차량/운전자 메뉴 화면(components/vehicle/*) 과 대시보드 위젯
(VehicleListWidget, DriverManagementWidget 등) 은 그대로 유지.

3 files changed, 525 deletions(-).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 07:52:38 +09:00

315 lines
10 KiB
TypeScript

import { useEffect, useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Camera, X } from "lucide-react";
import { ProfileFormData } from "@/types/profile";
import { apiClient } from "@/lib/api/client";
// 언어 정보 타입
interface LanguageInfo {
langCode: string;
langName: string;
langNative: string;
}
// 알림 모달 컴포넌트
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-emerald-600";
case "error":
return "text-destructive";
default:
return "text-primary";
}
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className={getTypeColor()}>{title}</DialogTitle>
</DialogHeader>
<div className="py-4">
<p className="text-sm text-muted-foreground">{message}</p>
</div>
<div className="flex justify-end">
<Button onClick={onClose} className="w-20">
</Button>
</div>
</DialogContent>
</Dialog>
);
}
interface ProfileModalProps {
isOpen: boolean;
user: any;
formData: ProfileFormData;
selectedImage: string;
isSaving: boolean;
departments: Array<{
dept_code: string;
dept_name: string;
}>;
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,
departments,
alertModal,
onClose,
onFormChange,
onImageSelect,
onImageRemove,
onSave,
onAlertClose,
}: ProfileModalProps) {
// 언어 목록 상태
const [languages, setLanguages] = useState<LanguageInfo[]>([]);
// 언어 목록 로드
useEffect(() => {
const loadLanguages = async () => {
try {
const response = await apiClient.get("/multilang/languages");
if (response.data?.success && response.data?.data) {
// is_active가 'Y'인 언어만 필터링하고 정렬
const activeLanguages = response.data.data
.filter((lang: any) => lang.isActive === "Y" || lang.is_active === "Y")
.map((lang: any) => ({
langCode: lang.langCode || lang.lang_code,
langName: lang.langName || lang.lang_name,
langNative: lang.langNative || lang.lang_native,
}))
.sort((a: LanguageInfo, b: LanguageInfo) => {
// KR을 먼저 표시
if (a.langCode === "KR") return -1;
if (b.langCode === "KR") return 1;
return a.langCode.localeCompare(b.langCode);
});
setLanguages(activeLanguages);
}
} catch (error) {
console.error("언어 목록 로드 실패:", error);
// 기본값 설정
setLanguages([
{ langCode: "KR", langName: "Korean", langNative: "한국어" },
{ langCode: "US", langName: "English", langNative: "English" },
]);
}
};
if (isOpen) {
loadLanguages();
}
}, [isOpen]);
return (
<>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle> </DialogTitle>
</DialogHeader>
<div className="grid gap-6 py-4">
{/* 프로필 사진 섹션 */}
<div className="flex flex-col items-center space-y-4">
<div className="relative">
<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"
/>
) : (
<div className="flex h-full w-full items-center justify-center rounded-full bg-slate-200 text-2xl font-semibold text-slate-700">
{formData.user_name?.substring(0, 1)?.toUpperCase() || "U"}
</div>
)}
</div>
{selectedImage && selectedImage.trim() !== "" ? (
<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>
) : null}
</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?.user_id || ""} disabled className="bg-muted" />
</div>
<div className="space-y-2">
<Label htmlFor="userName"></Label>
<Input
id="userName"
value={formData.user_name}
onChange={(e) => onFormChange("user_name", 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>
<Select value={formData.dept_name} onValueChange={(value) => onFormChange("dept_name", value)}>
<SelectTrigger>
<SelectValue placeholder="부서 선택" />
</SelectTrigger>
<SelectContent>
{Array.isArray(departments) && departments.length > 0 ? (
departments.map((department) => (
<SelectItem key={department.dept_code} value={department.dept_name}>
{department.dept_name}
</SelectItem>
))
) : (
<SelectItem value="no-data" disabled>
</SelectItem>
)}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="positionName"></Label>
<Input
id="positionName"
value={formData.position_name}
onChange={(e) => onFormChange("position_name", 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>
{languages.length > 0 ? (
languages.map((lang) => (
<SelectItem key={lang.langCode} value={lang.langCode}>
{lang.langNative} ({lang.langCode})
</SelectItem>
))
) : (
<SelectItem value="KR"> (KR)</SelectItem>
)}
</SelectContent>
</Select>
</div>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={onClose} disabled={isSaving}>
</Button>
<Button type="button" onClick={onSave} disabled={isSaving}>
{isSaving ? "저장 중..." : "저장"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 알림 모달 */}
<AlertModal
isOpen={alertModal.isOpen}
onClose={onAlertClose}
title={alertModal.title}
message={alertModal.message}
type={alertModal.type}
/>
</>
);
}