2025-09-01 17:57:52 +09:00
|
|
|
"use client";
|
|
|
|
|
|
2025-10-13 19:18:01 +09:00
|
|
|
import { useEffect, useMemo, useState, useRef } from "react";
|
2025-09-01 17:57:52 +09:00
|
|
|
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";
|
2025-10-13 19:18:01 +09:00
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
|
|
|
import { Search, X } from "lucide-react";
|
2025-09-01 17:57:52 +09:00
|
|
|
import { screenApi, tableTypeApi } from "@/lib/api/screen";
|
|
|
|
|
import { ScreenDefinition } from "@/types/screen";
|
|
|
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
|
|
|
|
|
|
|
|
interface CreateScreenModalProps {
|
|
|
|
|
open: boolean;
|
|
|
|
|
onOpenChange: (open: boolean) => void;
|
|
|
|
|
onCreated?: (screen: ScreenDefinition) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function CreateScreenModal({ open, onOpenChange, onCreated }: CreateScreenModalProps) {
|
|
|
|
|
const { user } = useAuth();
|
|
|
|
|
|
|
|
|
|
const [screenName, setScreenName] = useState("");
|
|
|
|
|
const [screenCode, setScreenCode] = useState("");
|
|
|
|
|
const [tableName, setTableName] = useState("");
|
|
|
|
|
const [description, setDescription] = useState("");
|
|
|
|
|
const [tables, setTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
|
|
|
|
|
const [submitting, setSubmitting] = useState(false);
|
2025-10-13 19:18:01 +09:00
|
|
|
const [tableSearchTerm, setTableSearchTerm] = useState("");
|
|
|
|
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
2025-09-01 17:57:52 +09:00
|
|
|
// 화면 코드 자동 생성
|
|
|
|
|
const generateCode = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const companyCode = (user as any)?.company_code || (user as any)?.companyCode || "*";
|
|
|
|
|
const generatedCode = await screenApi.generateScreenCode(companyCode);
|
|
|
|
|
setScreenCode(generatedCode);
|
|
|
|
|
} catch (e) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.error("화면 코드 생성 실패", e);
|
2025-09-01 17:57:52 +09:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!open) return;
|
|
|
|
|
let abort = false;
|
|
|
|
|
const loadTables = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const list = await tableTypeApi.getTables();
|
|
|
|
|
if (abort) return;
|
|
|
|
|
setTables(list.map((t) => ({ tableName: t.tableName, displayName: t.displayName || t.tableName })));
|
|
|
|
|
} catch (e) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.error("테이블 목록 조회 실패", e);
|
2025-09-01 17:57:52 +09:00
|
|
|
setTables([]);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
loadTables();
|
|
|
|
|
return () => {
|
|
|
|
|
abort = true;
|
|
|
|
|
};
|
|
|
|
|
}, [open]);
|
|
|
|
|
|
|
|
|
|
// 모달이 열릴 때 자동으로 화면 코드 생성
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (open && !screenCode) {
|
|
|
|
|
generateCode();
|
|
|
|
|
}
|
|
|
|
|
}, [open, screenCode]);
|
|
|
|
|
|
|
|
|
|
const isValid = useMemo(() => {
|
|
|
|
|
return screenName.trim().length > 0 && screenCode.trim().length > 0 && tableName.trim().length > 0;
|
|
|
|
|
}, [screenName, screenCode, tableName]);
|
|
|
|
|
|
2025-10-13 19:18:01 +09:00
|
|
|
// 테이블 필터링
|
|
|
|
|
const filteredTables = useMemo(() => {
|
|
|
|
|
if (!tableSearchTerm) return tables;
|
|
|
|
|
const searchLower = tableSearchTerm.toLowerCase();
|
|
|
|
|
return tables.filter(
|
|
|
|
|
(table) =>
|
|
|
|
|
table.displayName.toLowerCase().includes(searchLower) || table.tableName.toLowerCase().includes(searchLower),
|
|
|
|
|
);
|
|
|
|
|
}, [tables, tableSearchTerm]);
|
|
|
|
|
|
2025-09-01 17:57:52 +09:00
|
|
|
const handleSubmit = async () => {
|
|
|
|
|
if (!isValid || submitting) return;
|
|
|
|
|
try {
|
|
|
|
|
setSubmitting(true);
|
|
|
|
|
const companyCode = (user as any)?.company_code || (user as any)?.companyCode || "*";
|
|
|
|
|
const created = await screenApi.createScreen({
|
|
|
|
|
screenName: screenName.trim(),
|
|
|
|
|
screenCode: screenCode.trim(),
|
|
|
|
|
tableName: tableName.trim(),
|
|
|
|
|
companyCode,
|
|
|
|
|
description: description.trim() || undefined,
|
|
|
|
|
createdBy: (user as any)?.userId,
|
|
|
|
|
} as any);
|
|
|
|
|
|
|
|
|
|
// 날짜 필드 보정
|
|
|
|
|
const mapped: ScreenDefinition = {
|
|
|
|
|
...created,
|
|
|
|
|
createdDate: created.createdDate ? new Date(created.createdDate as any) : new Date(),
|
|
|
|
|
updatedDate: created.updatedDate ? new Date(created.updatedDate as any) : new Date(),
|
|
|
|
|
} as ScreenDefinition;
|
|
|
|
|
|
|
|
|
|
onCreated?.(mapped);
|
|
|
|
|
onOpenChange(false);
|
|
|
|
|
setScreenName("");
|
|
|
|
|
setScreenCode("");
|
|
|
|
|
setTableName("");
|
|
|
|
|
setDescription("");
|
|
|
|
|
} catch (e) {
|
2025-10-01 18:17:30 +09:00
|
|
|
// console.error("화면 생성 실패", e);
|
2025-09-01 17:57:52 +09:00
|
|
|
// 필요 시 토스트 추가 가능
|
|
|
|
|
} finally {
|
|
|
|
|
setSubmitting(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
|
|
|
<DialogContent className="sm:max-w-lg">
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>새 화면 생성</DialogTitle>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="screenName">화면명</Label>
|
|
|
|
|
<Input id="screenName" value={screenName} onChange={(e) => setScreenName(e.target.value)} />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="screenCode">화면 코드</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="screenCode"
|
|
|
|
|
value={screenCode}
|
|
|
|
|
readOnly
|
|
|
|
|
placeholder="자동 생성됩니다..."
|
|
|
|
|
className="cursor-not-allowed bg-gray-50"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="tableName">테이블</Label>
|
2025-10-13 19:18:01 +09:00
|
|
|
<Select
|
2025-09-01 17:57:52 +09:00
|
|
|
value={tableName}
|
2025-10-13 19:18:01 +09:00
|
|
|
onValueChange={setTableName}
|
|
|
|
|
onOpenChange={(open) => {
|
|
|
|
|
if (open) {
|
|
|
|
|
// Select가 열릴 때 검색창에 포커스
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
searchInputRef.current?.focus();
|
|
|
|
|
}, 100);
|
|
|
|
|
}
|
|
|
|
|
}}
|
2025-09-01 17:57:52 +09:00
|
|
|
>
|
2025-10-13 19:18:01 +09:00
|
|
|
<SelectTrigger className="w-full">
|
|
|
|
|
<SelectValue placeholder="테이블을 선택하세요" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent className="max-h-80">
|
|
|
|
|
{/* 검색 입력 필드 */}
|
|
|
|
|
<div
|
|
|
|
|
className="sticky top-0 z-10 border-b bg-white p-2"
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
// 이 div 내에서 발생하는 모든 키 이벤트를 차단
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
|
|
|
|
|
<input
|
|
|
|
|
ref={searchInputRef}
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="테이블명으로 검색..."
|
|
|
|
|
value={tableSearchTerm}
|
|
|
|
|
autoFocus
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setTableSearchTerm(e.target.value);
|
|
|
|
|
}}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
// 이벤트가 Select로 전파되지 않도록 완전 차단
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
}}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
onFocus={(e) => e.stopPropagation()}
|
|
|
|
|
onMouseDown={(e) => e.stopPropagation()}
|
|
|
|
|
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-8 w-full rounded-md border px-3 py-2 pr-8 pl-10 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
|
|
|
/>
|
|
|
|
|
{tableSearchTerm && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setTableSearchTerm("");
|
|
|
|
|
}}
|
|
|
|
|
className="hover:text-muted-foreground absolute top-1/2 right-2 h-4 w-4 -translate-y-1/2 transform text-gray-400"
|
|
|
|
|
>
|
|
|
|
|
<X className="h-3 w-3" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 테이블 옵션들 */}
|
|
|
|
|
<div className="max-h-60 overflow-y-auto">
|
|
|
|
|
{filteredTables.length === 0 ? (
|
|
|
|
|
<div className="px-2 py-6 text-center text-sm text-gray-500">
|
|
|
|
|
{tableSearchTerm ? `"${tableSearchTerm}"에 대한 검색 결과가 없습니다` : "테이블이 없습니다"}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
filteredTables.map((table) => (
|
|
|
|
|
<SelectItem key={table.tableName} value={table.tableName}>
|
|
|
|
|
{table.displayName} ({table.tableName})
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2025-09-01 17:57:52 +09:00
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="description">설명</Label>
|
|
|
|
|
<Input id="description" value={description} onChange={(e) => setDescription(e.target.value)} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<DialogFooter className="mt-4">
|
|
|
|
|
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={submitting}>
|
|
|
|
|
취소
|
|
|
|
|
</Button>
|
2025-10-02 14:34:15 +09:00
|
|
|
<Button onClick={handleSubmit} disabled={!isValid || submitting} variant="default">
|
2025-09-01 17:57:52 +09:00
|
|
|
생성
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
}
|