Skip to content

Commit 1ad49cc

Browse files
limeburstclaude
andcommitted
control-plane: Move image zoom controls to header, hide save for images
Expose zoom controls from ImageViewer via ref, pipe them through FileViewer, and render zoom/save buttons conditionally in the filename header based on file type. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8a825f8 commit 1ad49cc

File tree

4 files changed

+101
-75
lines changed

4 files changed

+101
-75
lines changed

control-plane/src/components/browser/FileExplorer.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -423,9 +423,26 @@ export default function FileExplorer({ initialFiles, userLoginName }: FileExplor
423423
}
424424
})()}
425425
</div>
426-
<Button size="sm" onClick={() => fileViewerRef.current?.save()}>
427-
저장
428-
</Button>
426+
{(() => {
427+
const ext = selectedFile.split('.').pop()?.toLowerCase() || '';
428+
if (EDITABLE_FILE_EXTENSIONS.includes(ext)) {
429+
return (
430+
<Button size="sm" onClick={() => fileViewerRef.current?.save()}>
431+
저장
432+
</Button>
433+
);
434+
}
435+
if (IMAGE_FILE_EXTENSIONS.includes(ext)) {
436+
return (
437+
<div className="flex items-center gap-2">
438+
<Button variant="outline" size="sm" onClick={() => fileViewerRef.current?.zoomOut()}>축소</Button>
439+
<Button variant="outline" size="sm" onClick={() => fileViewerRef.current?.resetZoom()}>원본</Button>
440+
<Button variant="outline" size="sm" onClick={() => fileViewerRef.current?.zoomIn()}>확대</Button>
441+
</div>
442+
);
443+
}
444+
return null;
445+
})()}
429446
</>
430447
)}
431448
</div>

control-plane/src/components/browser/FileExplorerWithSelected.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import DirectoryTree from "./DirectoryTree";
55
import FileViewer, { FileViewerRef } from "./FileViewer";
66
import { FileNode } from "@/lib/fileUtils";
77
import { Button } from "@/components/ui/button";
8+
import { EDITABLE_FILE_EXTENSIONS, IMAGE_FILE_EXTENSIONS } from "@/lib/const";
89

910
interface FileExplorerWithSelectedProps {
1011
initialFiles: FileNode[];
@@ -99,11 +100,26 @@ export default function FileExplorerWithSelected({
99100
<h3 className="font-medium text-foreground">
100101
{selectedFile ? `📄 ${selectedFile.split('/').pop()}` : "파일을 선택하세요"}
101102
</h3>
102-
{selectedFile && (
103-
<Button size="sm" onClick={() => fileViewerRef.current?.save()}>
104-
저장
105-
</Button>
106-
)}
103+
{selectedFile && (() => {
104+
const ext = selectedFile.split('.').pop()?.toLowerCase() || '';
105+
if (EDITABLE_FILE_EXTENSIONS.includes(ext)) {
106+
return (
107+
<Button size="sm" onClick={() => fileViewerRef.current?.save()}>
108+
저장
109+
</Button>
110+
);
111+
}
112+
if (IMAGE_FILE_EXTENSIONS.includes(ext)) {
113+
return (
114+
<div className="flex items-center gap-2">
115+
<Button variant="outline" size="sm" onClick={() => fileViewerRef.current?.zoomOut()}>축소</Button>
116+
<Button variant="outline" size="sm" onClick={() => fileViewerRef.current?.resetZoom()}>원본</Button>
117+
<Button variant="outline" size="sm" onClick={() => fileViewerRef.current?.zoomIn()}>확대</Button>
118+
</div>
119+
);
120+
}
121+
return null;
122+
})()}
107123
</div>
108124
<div className="flex-1 min-h-0 overflow-auto">
109125
{selectedFile ? (

control-plane/src/components/browser/FileViewer.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
"use client";
22

3-
import { useState, useEffect, useImperativeHandle, forwardRef } from "react";
3+
import { useState, useEffect, useImperativeHandle, forwardRef, useRef } from "react";
44
import { EDITABLE_FILE_EXTENSIONS, IMAGE_FILE_EXTENSIONS, AUDIO_FILE_EXTENSIONS } from "@/lib/const";
55
import { getPublicAssetUrl } from "@/lib/utils";
66
import Editor from "@/components/Editor";
7-
import ImageViewer from "./ImageViewer";
7+
import ImageViewer, { ImageViewerRef } from "./ImageViewer";
88
import { Button } from "@/components/ui/button";
99
import { toast } from "sonner";
1010

11+
export type FileType = "editable" | "image" | "audio" | "other";
12+
1113
export interface FileViewerRef {
14+
fileType: FileType;
1215
save: () => Promise<void>;
16+
zoomIn: () => void;
17+
zoomOut: () => void;
18+
resetZoom: () => void;
19+
getZoom: () => number;
1320
}
1421

1522
interface FileViewerProps {
@@ -22,6 +29,7 @@ const FileViewer = forwardRef<FileViewerRef, FileViewerProps>(function FileViewe
2229
const [loading, setLoading] = useState(true);
2330
const [error, setError] = useState<string | null>(null);
2431
const [editorValue, setEditorValue] = useState<string>("");
32+
const imageViewerRef = useRef<ImageViewerRef>(null);
2533

2634
const fileName = filePath.split('/').pop() || '';
2735
const fileExtension = fileName.split('.').pop()?.toLowerCase() || '';
@@ -81,9 +89,16 @@ const FileViewer = forwardRef<FileViewerRef, FileViewerProps>(function FileViewe
8189
}
8290
};
8391

92+
const fileType: FileType = isEditable ? "editable" : isImage ? "image" : isAudio ? "audio" : "other";
93+
8494
useImperativeHandle(ref, () => ({
95+
fileType,
8596
save: handleSave,
86-
}), [editorValue, filePath]);
97+
zoomIn: () => imageViewerRef.current?.zoomIn(),
98+
zoomOut: () => imageViewerRef.current?.zoomOut(),
99+
resetZoom: () => imageViewerRef.current?.resetZoom(),
100+
getZoom: () => imageViewerRef.current?.getZoom() ?? 1,
101+
}), [editorValue, filePath, fileType]);
87102

88103
if (loading) {
89104
return (
@@ -127,6 +142,7 @@ const FileViewer = forwardRef<FileViewerRef, FileViewerProps>(function FileViewe
127142
const imageSrc = getPublicAssetUrl(userLoginName, filePath);
128143
return (
129144
<ImageViewer
145+
ref={imageViewerRef}
130146
src={imageSrc}
131147
alt={fileName}
132148
filename={fileName}
Lines changed: 41 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
"use client";
22

3-
import { useState } from "react";
3+
import { useState, forwardRef, useImperativeHandle } from "react";
44
import Image from "next/image";
5-
import { Button } from "@/components/ui/button";
65

76
interface ImageViewerProps {
87
src: string;
98
alt: string;
109
filename: string;
1110
}
1211

13-
export default function ImageViewer({ src, alt, filename }: ImageViewerProps) {
12+
export interface ImageViewerRef {
13+
zoomIn: () => void;
14+
zoomOut: () => void;
15+
resetZoom: () => void;
16+
getZoom: () => number;
17+
}
18+
19+
const ImageViewer = forwardRef<ImageViewerRef, ImageViewerProps>(function ImageViewer({ src, alt, filename }, ref) {
1420
const [zoom, setZoom] = useState(1);
1521
const [imageError, setImageError] = useState(false);
1622

@@ -26,6 +32,13 @@ export default function ImageViewer({ src, alt, filename }: ImageViewerProps) {
2632
setZoom(1);
2733
};
2834

35+
useImperativeHandle(ref, () => ({
36+
zoomIn: handleZoomIn,
37+
zoomOut: handleZoomOut,
38+
resetZoom: handleResetZoom,
39+
getZoom: () => zoom,
40+
}), [zoom]);
41+
2942
if (imageError) {
3043
return (
3144
<div className="flex items-center justify-center h-full text-muted-foreground">
@@ -38,69 +51,33 @@ export default function ImageViewer({ src, alt, filename }: ImageViewerProps) {
3851
}
3952

4053
return (
41-
<div className="h-full flex flex-col bg-muted">
42-
{/* Image Controls */}
43-
<div className="p-3 border-b border-border bg-card">
44-
<div className="flex items-center justify-end">
45-
<div className="flex items-center gap-2">
46-
<Button
47-
variant="outline"
48-
size="sm"
49-
onClick={handleZoomOut}
50-
className="text-xs px-2 py-1"
51-
>
52-
축소
53-
</Button>
54-
<span className="text-xs text-muted-foreground min-w-[4rem] text-center">
55-
{Math.round(zoom * 100)}%
56-
</span>
57-
<Button
58-
variant="outline"
59-
size="sm"
60-
onClick={handleZoomIn}
61-
className="text-xs px-2 py-1"
62-
>
63-
확대
64-
</Button>
65-
<Button
66-
variant="outline"
67-
size="sm"
68-
onClick={handleResetZoom}
69-
className="text-xs px-2 py-1"
70-
>
71-
원본
72-
</Button>
73-
</div>
74-
</div>
75-
</div>
76-
77-
{/* Image Display */}
78-
<div className="flex-1 overflow-auto p-4">
79-
<div className="flex items-center justify-center min-h-full">
80-
<div
81-
style={{
82-
transform: `scale(${zoom})`,
83-
transformOrigin: 'center center',
84-
transition: 'transform 0.2s ease-out'
54+
<div className="h-full overflow-auto p-4 bg-muted">
55+
<div className="flex items-center justify-center min-h-full">
56+
<div
57+
style={{
58+
transform: `scale(${zoom})`,
59+
transformOrigin: 'center center',
60+
transition: 'transform 0.2s ease-out'
61+
}}
62+
className="max-w-full max-h-full"
63+
>
64+
<img
65+
src={src}
66+
alt={alt}
67+
onError={() => setImageError(true)}
68+
className="max-w-full max-h-full border border-border rounded"
69+
style={{
70+
display: 'block',
71+
maxWidth: zoom === 1 ? '100%' : 'none',
72+
maxHeight: zoom === 1 ? '100%' : 'none',
73+
width: zoom === 1 ? 'auto' : undefined,
74+
height: zoom === 1 ? 'auto' : undefined
8575
}}
86-
className="max-w-full max-h-full"
87-
>
88-
<img
89-
src={src}
90-
alt={alt}
91-
onError={() => setImageError(true)}
92-
className="max-w-full max-h-full border border-border rounded"
93-
style={{
94-
display: 'block',
95-
maxWidth: zoom === 1 ? '100%' : 'none',
96-
maxHeight: zoom === 1 ? '100%' : 'none',
97-
width: zoom === 1 ? 'auto' : undefined,
98-
height: zoom === 1 ? 'auto' : undefined
99-
}}
100-
/>
101-
</div>
76+
/>
10277
</div>
10378
</div>
10479
</div>
10580
);
106-
}
81+
});
82+
83+
export default ImageViewer;

0 commit comments

Comments
 (0)