fix(WEB): 모바일 반응형 UI 개선 및 개소 정보 수정 모달 추가
- CalendarHeader: 모바일에서 주/월 버튼 2줄 레이아웃으로 분리 - MobileCard: 제목 텍스트 overflow 시 truncate 적용 - DetailActions: 모바일 하단 sticky 버튼 바 overflow 수정 - OrderDetailForm: 모바일 하단 sticky 버튼 바 overflow 수정 - LocationDetailPanel: 오픈사이즈 옆 수정 버튼에 모달 연결 - LocationListPanel: 개소 목록에 수정/삭제 버튼 추가 - LocationEditModal: 개소 정보 수정 팝업 신규 생성 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
||||
} from "../ui/table";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
||||
import { ItemSearchModal } from "./ItemSearchModal";
|
||||
import { LocationEditModal } from "./LocationEditModal";
|
||||
|
||||
import type { LocationItem } from "./QuoteRegistrationV2";
|
||||
import type { FinishedGoods } from "./actions";
|
||||
@@ -117,6 +118,7 @@ export function LocationDetailPanel({
|
||||
|
||||
const [activeTab, setActiveTab] = useState("body");
|
||||
const [itemSearchOpen, setItemSearchOpen] = useState(false);
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 계산된 값
|
||||
@@ -257,7 +259,14 @@ export function LocationDetailPanel({
|
||||
className="w-24 h-8 text-center font-bold"
|
||||
/>
|
||||
{!disabled && (
|
||||
<Badge variant="secondary" className="text-xs">수정</Badge>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="text-xs h-7"
|
||||
onClick={() => setEditModalOpen(true)}
|
||||
>
|
||||
수정
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -613,6 +622,16 @@ export function LocationDetailPanel({
|
||||
}}
|
||||
tabLabel={DETAIL_TABS.find((t) => t.value === activeTab)?.label}
|
||||
/>
|
||||
|
||||
{/* 개소 정보 수정 모달 */}
|
||||
<LocationEditModal
|
||||
open={editModalOpen}
|
||||
onOpenChange={setEditModalOpen}
|
||||
location={location}
|
||||
onSave={(locationId, updates) => {
|
||||
onUpdateLocation(locationId, updates);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
282
src/components/quotes/LocationEditModal.tsx
Normal file
282
src/components/quotes/LocationEditModal.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* 개소 정보 수정 모달
|
||||
*
|
||||
* 발주 개소 목록에서 수정 버튼 클릭 시 표시
|
||||
* - 개소 정보: 층, 부호
|
||||
* - 오픈사이즈: 가로, 세로
|
||||
* - 필수 설정: 가이드레일, 전원, 제어기
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../ui/dialog";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { NumberInput } from "../ui/number-input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../ui/select";
|
||||
|
||||
import type { LocationItem } from "./QuoteRegistrationV2";
|
||||
|
||||
// =============================================================================
|
||||
// 상수
|
||||
// =============================================================================
|
||||
|
||||
// 가이드레일 설치 유형
|
||||
const GUIDE_RAIL_TYPES = [
|
||||
{ value: "wall", label: "벽면형" },
|
||||
{ value: "floor", label: "측면형" },
|
||||
];
|
||||
|
||||
// 모터 전원
|
||||
const MOTOR_POWERS = [
|
||||
{ value: "single", label: "단상(220V)" },
|
||||
{ value: "three", label: "삼상(380V)" },
|
||||
];
|
||||
|
||||
// 연동제어기
|
||||
const CONTROLLERS = [
|
||||
{ value: "basic", label: "단독" },
|
||||
{ value: "smart", label: "연동" },
|
||||
{ value: "premium", label: "매립형-뒷박스포함" },
|
||||
];
|
||||
|
||||
// =============================================================================
|
||||
// Props
|
||||
// =============================================================================
|
||||
|
||||
interface LocationEditModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
location: LocationItem | null;
|
||||
onSave: (locationId: string, updates: Partial<LocationItem>) => void;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 컴포넌트
|
||||
// =============================================================================
|
||||
|
||||
export function LocationEditModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
location,
|
||||
onSave,
|
||||
}: LocationEditModalProps) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// 상태
|
||||
// ---------------------------------------------------------------------------
|
||||
const [formData, setFormData] = useState({
|
||||
floor: "",
|
||||
code: "",
|
||||
openWidth: 0,
|
||||
openHeight: 0,
|
||||
guideRailType: "wall",
|
||||
motorPower: "single",
|
||||
controller: "basic",
|
||||
});
|
||||
|
||||
// location 변경 시 폼 데이터 초기화
|
||||
useEffect(() => {
|
||||
if (location) {
|
||||
setFormData({
|
||||
floor: location.floor,
|
||||
code: location.code,
|
||||
openWidth: location.openWidth,
|
||||
openHeight: location.openHeight,
|
||||
guideRailType: location.guideRailType,
|
||||
motorPower: location.motorPower,
|
||||
controller: location.controller,
|
||||
});
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 핸들러
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const handleFieldChange = (field: string, value: string | number) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!location) return;
|
||||
|
||||
onSave(location.id, {
|
||||
floor: formData.floor,
|
||||
code: formData.code,
|
||||
openWidth: formData.openWidth,
|
||||
openHeight: formData.openHeight,
|
||||
guideRailType: formData.guideRailType,
|
||||
motorPower: formData.motorPower,
|
||||
controller: formData.controller,
|
||||
});
|
||||
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 렌더링
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
if (!location) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>개소 정보 수정</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 py-4">
|
||||
{/* 개소 정보 */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold text-gray-700">개소 정보</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-gray-600 mb-1 block">층</label>
|
||||
<Input
|
||||
value={formData.floor}
|
||||
onChange={(e) => handleFieldChange("floor", e.target.value)}
|
||||
placeholder="1층"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-600 mb-1 block">부호</label>
|
||||
<Input
|
||||
value={formData.code}
|
||||
onChange={(e) => handleFieldChange("code", e.target.value)}
|
||||
placeholder="FSS-01"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 오픈사이즈 */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold text-gray-700">오픈사이즈</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-gray-600 mb-1 block">가로 (mm)</label>
|
||||
<NumberInput
|
||||
value={formData.openWidth}
|
||||
onChange={(value) => handleFieldChange("openWidth", value ?? 0)}
|
||||
placeholder="5000"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-600 mb-1 block">세로 (mm)</label>
|
||||
<NumberInput
|
||||
value={formData.openHeight}
|
||||
onChange={(value) => handleFieldChange("openHeight", value ?? 0)}
|
||||
placeholder="3000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
※ 제작사이즈는 오픈사이즈의 280mm를 더한 값으로 자동 계산됩니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 필수 설정 */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold text-gray-700">필수 설정</h4>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 flex items-center gap-1 mb-1">
|
||||
🔧 가이드레일
|
||||
</label>
|
||||
<Select
|
||||
value={formData.guideRailType}
|
||||
onValueChange={(value) => handleFieldChange("guideRailType", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{GUIDE_RAIL_TYPES.map((type) => (
|
||||
<SelectItem key={type.value} value={type.value}>
|
||||
{type.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 flex items-center gap-1 mb-1">
|
||||
⚡ 전원
|
||||
</label>
|
||||
<Select
|
||||
value={formData.motorPower}
|
||||
onValueChange={(value) => handleFieldChange("motorPower", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{MOTOR_POWERS.map((power) => (
|
||||
<SelectItem key={power.value} value={power.value}>
|
||||
{power.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 flex items-center gap-1 mb-1">
|
||||
📦 제어기
|
||||
</label>
|
||||
<Select
|
||||
value={formData.controller}
|
||||
onValueChange={(value) => handleFieldChange("controller", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{CONTROLLERS.map((ctrl) => (
|
||||
<SelectItem key={ctrl.value} value={ctrl.value}>
|
||||
{ctrl.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 버튼 */}
|
||||
<div className="flex gap-3 pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
className="flex-1"
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="flex-1 bg-orange-500 hover:bg-orange-600"
|
||||
>
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { Plus, Upload, Download, Trash2 } from "lucide-react";
|
||||
import { Plus, Upload, Download, Trash2, Pencil } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "../ui/button";
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
TableRow,
|
||||
} from "../ui/table";
|
||||
import { DeleteConfirmDialog } from "../ui/confirm-dialog";
|
||||
import { LocationEditModal } from "./LocationEditModal";
|
||||
|
||||
import type { LocationItem } from "./QuoteRegistrationV2";
|
||||
import type { FinishedGoods } from "./actions";
|
||||
@@ -70,6 +71,7 @@ interface LocationListPanelProps {
|
||||
onSelectLocation: (id: string) => void;
|
||||
onAddLocation: (location: Omit<LocationItem, "id">) => void;
|
||||
onDeleteLocation: (id: string) => void;
|
||||
onUpdateLocation: (locationId: string, updates: Partial<LocationItem>) => void;
|
||||
onExcelUpload: (locations: Omit<LocationItem, "id">[]) => void;
|
||||
finishedGoods: FinishedGoods[];
|
||||
disabled?: boolean;
|
||||
@@ -85,6 +87,7 @@ export function LocationListPanel({
|
||||
onSelectLocation,
|
||||
onAddLocation,
|
||||
onDeleteLocation,
|
||||
onUpdateLocation,
|
||||
onExcelUpload,
|
||||
finishedGoods,
|
||||
disabled = false,
|
||||
@@ -109,6 +112,9 @@ export function LocationListPanel({
|
||||
// 삭제 확인 다이얼로그
|
||||
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
|
||||
|
||||
// 수정 모달
|
||||
const [editTarget, setEditTarget] = useState<LocationItem | null>(null);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 핸들러
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -346,17 +352,30 @@ export function LocationListPanel({
|
||||
<TableCell className="text-center">{loc.quantity}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{!disabled && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-red-500 hover:text-red-600"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDeleteTarget(loc.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-gray-500 hover:text-blue-600"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditTarget(loc);
|
||||
}}
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-red-500 hover:text-red-600"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDeleteTarget(loc.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -521,6 +540,18 @@ export function LocationListPanel({
|
||||
title="개소 삭제"
|
||||
description="선택한 개소를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
|
||||
/>
|
||||
|
||||
{/* 개소 정보 수정 모달 */}
|
||||
<LocationEditModal
|
||||
open={!!editTarget}
|
||||
onOpenChange={(open) => !open && setEditTarget(null)}
|
||||
location={editTarget}
|
||||
onSave={(locationId, updates) => {
|
||||
onUpdateLocation(locationId, updates);
|
||||
setEditTarget(null);
|
||||
toast.success("개소 정보가 수정되었습니다.");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -703,6 +703,7 @@ export function QuoteRegistrationV2({
|
||||
onSelectLocation={setSelectedLocationId}
|
||||
onAddLocation={handleAddLocation}
|
||||
onDeleteLocation={handleDeleteLocation}
|
||||
onUpdateLocation={handleUpdateLocation}
|
||||
onExcelUpload={handleExcelUpload}
|
||||
finishedGoods={finishedGoods}
|
||||
disabled={isViewMode}
|
||||
|
||||
Reference in New Issue
Block a user