์ค๋์ ์ฝ๋ํจ๋์ ๋ฆฌ์กํธ ๋ผ์ด๋ธ์ฝ๋ฉ ์์ ๋ฌธ์ ๋ฅผ ํ๋ฒ ํ์ด๋ณด๋ ค๊ณ ํ๋ค! ใ ใ
๋ฌธ์ ์ํฉ
Today, we are tasked with building a File Tree UI given a list of File objects, consisting of a path and the contents of the file. Your solution must:
- display files in a nested structure, with entries for each folder and file
- be able to handle arbitrarily-deep file structures
- sort the file tree by the following rules:
- folders are shown before files
- all items are sorted alphabetically (case-insensitive)
If you solve this challenge and we still have more time, our follow-on task is to implement the ability to add new files to the file tree.
์๊ตฌ์ฌํญ ๋ถ์
1. nested structure ๋ฅผ ๋ณด์ฌ์ค ๊ฒ
๐ src
โโโ App.js
โโโ ๐ components
โ โโโ Button.js
โโโ ๐ utils
โโโ helpers.js
๐ public
โโโ index.html
2. arbitrarily-deep file structures ( ์ผ๋ง๋ ์ง ๊น์ด๊ฐ ๊น์ด์ง ์ ์๋ ํด๋ ๊ตฌ์กฐ ) ๋ ์ ์ฒ๋ฆฌํ ์ ์์ด์ผ ํ๋ค.
3. file tree ๋ฅผ ๋ค์ ๊ธฐ์ค์ผ๋ก ์ ๋ ฌํ๋ค.
- ํด๋๋ ํ์ผ ์ด์ ์ ๋ณด์ฌ์ ธ์ผํ๋ค.
- ๋ชจ๋ ์์ดํ ๋ค์ ์ํ๋ฒณ์์ผ๋ก ์ ๋ ฌ๋์ด์ผํ๋ค.
๊ตฌํ ์์ด๋์ด
ํ์ผ ์์คํ ์์ ๊ตฌ์กฐ๊ฐ ํธ๋ฆฌ ์๋ฃ๊ตฌ์กฐ๋ก ์ฒ๋ฆฌ๋๋ค๋ ๊ฒ์ ์ด์์ฒด์ ์์ ๋ฐฐ์์ ์๊ณ ์์๋ค.
ํธ๋ฆฌ๊ฐ ํ๋์ ๋ถ๋ชจ์ ๋ํด ์ฌ๋ฌ๊ฐ์ ์์์ ๊ฐ์ง ์ ์๋ ๊ตฌ์กฐ์ด๋, ํ์ผํธ๋ฆฌ๋ฅผ ํํํ๊ธฐ์ ์ ํฉํ๋ค.
์ฃผ์ด์ง ๊ตฌํ์ ํด๋ด๋ฉด ์๋ง ์ด๋ฐ ๋๋์ ํธ๋ฆฌ๊ฐ ์์ฑ๋ ๊ฒ์ด๋ค.
์ธํ์ ํ๋ํ๋ ๋๋ฉด์ ํธ๋ฆฌ ๊ตฌ์กฐ๋ฅผ ํ๋ํ๋ ์์ฑํด๋๊ฐ๋ฉด ๋ ๊ฒ ๊ฐ๋ค๊ณ ์๊ฐํ๋ค!
๊ทธ๋ฐ๋ฐ ์ฌ๊ธฐ์ ๋ฌธ์ ๋ ์ฃผ์ด์ง ์ธํ์ ์ฒ๋ฆฌํด์ ํธ๋ฆฌ๊ตฌ์กฐ๋ฅผ ๋ง๋๋ ๊ฒ์ด ์กฐ๊ธ ๊น๋ค๋ก์ธ ์ ์๋ค.
ํ์ผํธ๋ฆฌ ์์ฑํ๊ธฐ
๋ณดํต์ ํธ๋ฆฌ๋ฅผ ๊ตฌ์ฑํ ๋ ๊ฐ์ฅ ์ด์์ ์ธ ์ธํ์ ๋ถ๋ชจ -> ์์ ๊ด๊ณ์ ๋ํ ๊ฐ์ ์ผ๋ก ์ฃผ์ด์ก๋ค.
app/src/WidgetList/WidgetList.tsx ๊ฐ ์๋ค๊ณ ํ๋ฉด
app->src, src->WidgetList, WidgetList->WidgetList.tsx ๋ก ์ชผ๊ฐ์ด์ ๋ถ๋ชจ - ์์ ๊ด๊ณ๋ฅผ ์ฐ๊ฒฐํด์ฃผ๋ฉด ๋๋ค.
๋ฌผ๋ก ์ด๋๋ ์ค๋ณต๋๋ ์์๊ฐ ๋ค์ด์ฌ ์ ์์ผ๋ ์ด๋ฅผ ์ ์ฒ๋ฆฌํด์ฃผ์ด์ผํ๊ณ , ์ฃผ์ด์ง ๊ฐ์ ๋ค์ ํ๋์ฉ ์ฒ๋ฆฌํ๋ฉด์ ํธ๋ฆฌ๊ตฌ์กฐ๋ฅผ ๋ง๋ค๋ฉด ๋๋ค.
type TreeNode = {
name: string;
type: 'folder' | 'file';
children?: TreeNode[]; // folder
file?:File;
};
const isFile = (path) => path.includes('.');
const buildFileTree = (files: File[]): TreeNode => {
const root: TreeNode = { name: '', type: 'folder', children: [] };
// building trees ( ํ์ผ ํ๋ํ๋ ๋๋ฉด์ ํธ๋ฆฌ ๋ง๋ค๊ธฐ )
for (const file of files) {
var tokens = file.path.split('/');
let current = root;
for(let i=0;i<tokens.length;i++){
let token = tokens[i];
// current์ ์ด๋ฆ์ด token ์ธ ์์์ด ์๋์ง ํ์ธ
let existing = current.children!.find((c)=>c.name ===token);
if(!existing){
const type = isFile(token) ? 'file' as const : 'folder' as const;
const newNode = {
name :tokens[i], type, ...(isFile(token) ? { file: file } : { children: [] })
}
current.children!.push(newNode);
current = newNode;
}else{
current = existing;
}
}
}
return root;
}
๊ทธ ๊ฒฐ๊ณผ ๋ค์๊ณผ ๊ฐ์ ํธ๋ฆฌ ์๋ฃ๊ตฌ์กฐ๊ฐ ๋ง๋ค์ด์ง๋ค!
ํ์ผ ํธ๋ฆฌ ๋ ๋๋ง
๊ทธ ๋ค์, ๋ง๋ ํธ๋ฆฌ๊ตฌ์กฐ๋ฅผ root -> left -> right ์์ผ๋ก ์ฌ๊ท์ ์ผ๋ก ํธ์ถํ๋ฉด์ ๋ ๋๋ง์์ผ์ฃผ๋ฉด ๋๋ค.
ํ์ผ ํธ๋ฆฌ๋ฅผ ์ผ์ชฝ๋ถํฐ ์ฐจ๊ทผ์ฐจ๊ทผ DFS ํ์์ ํ๋ค๊ณ ์๊ฐํ๋ฉด ํธํ๋ค! ๊ฒฐ๊ตญ ์ ์์ํ์ธ๊ฒ!
ํธ๋ฆฌ์ ๊ฐ ๋ ธ๋๋ฅผ ํ์ํ๋ฉด์
1. ํ์ผ์ด๋ผ๋ฉด ๋ฐ๋ก ๋ ๋๋ง
2. ํด๋๋ผ๋ฉด ๊ฐ ์์๋ ธ๋๋ค์ root ๋ก ํ์ฌ ๋ณธ๊ณผ์ ๋ค์ ๋ฐ๋ณต (์ฌ๊ท)
export const FileNode = ({root,depth}) =>{
if(root.type =='file'){
console.log("file",root);
return <Box display="flex" justifyContent={"flex-start"}
><span style={{marginLeft:depth*20-15}}/><FileRow file={root.file} /></Box>
}
else if(root.type=='folder'){
console.log(root.children);
return (<Box><span style={{marginLeft:depth*20}}> ใด ๐ {root.name}</span>
<Box />
{root.children?.map((c)=><FileNode depth={depth+1} root={c} key={c.path}/>)}
</Box>)
}
return null;
}
export const FilePane = () => {
const { files } = useWorkspaceContext()
const root = buildFileTree (files);
console.log(root)
return (<Box>
<Box p={1}>
<Typography variant="h6">Files</Typography>
{root.children?.map((c:TreeNode)=><FileNode depth={0} root={c} key={c.name}/>)}
</Box>
</Box>)
}
์ฌ๊ธฐ์ ํฌ์ธํธ๋ ํด๋์ ๊น์ด์ ๋น๋กํด์ ์ผ์ชฝ ๋ง์ง ๊ฐ์ ์ฃผ์ด์ผํ๋ค๋ ๊ฒ! ๋ฐ๋ผ์ ๋งค ์ฌ๊ทํธ์ถ์ depth ๋ผ๋ ์ธ์๋ฅผ ์ถ๊ฐํ์ฌ
depth*20 ๋งํผ ์์ ๋น์๋๋๋ก ํ๋ค.
์ต์ข ๊ตฌํ ๊ฒฐ๊ณผ
ํธ๋ฆฌ ํ์ผ ๊ณ์ธต ๊ตฌ์กฐ๋ก ๋ํ๋ ๋ชจ์ต!
์ถ๊ฐ๊ธฐ๋ฅ
๋ฉ์ธ ๊ธฐ๋ฅ์ ์์ฑํ ์ดํ์๋ ์ถ๊ฐ ๊ธฐ๋ฅ์ ์์ฑํด ๋ณผ ๊ฒ์ด๋ค.
1. nested structure ์ ์ ๋ ฌ๋ ์ํ๋ก ๋ณด์ฌ์ค ๊ฒ
sort the file tree by the following rules:
folders are shown before files
all items are sorted alphabetically (case-insensitive)
=> ํธ๋ฆฌ๋ฅผ ์ํํ๋ฉด์, ๋ชจ๋ ๋ถ๋ชจ๋ ธ๋๋ค์ ๋ํ์ฌ ์์ ๋ ธ๋๋ค์ ์ํ๋ฒณ ์์ผ๋ก ์ ๋ ฌํด์ฃผ๋ฉด ๋ ๊ฒ ๊ฐ๋ค!
2. ํ์ผ ์ถ๊ฐํ๊ธฐ(add File) ๊ธฐ๋ฅ
=> ์ ์ฒด ํ์ผ ๋ชฉ๋ก์ state ๋ก ๊ด๋ฆฌํ๊ณ , HTML input ํ๊ทธ๋ฅผ ํตํด์ ๋ก์ปฌ ์ปดํจํฐ๋ก๋ถํฐ ํ์ผ์ ๋ฐ์์จ ํ,
ํด๋น ํ์ผ์ ์ ๊ทผํ์ฌ ์ ๋ชฉ, ๋ด์ฉ ๋ฑ์ ์์๋ด์ด ์ ์ฒด ํ์ผ ๋ชฉ๋ก์ ์ถ๊ฐ์ํค๋ฉด ๋ ๊ฒ ๊ฐ๋ค!
์ ๋ ฌํ๊ธฐ
ํ์ผํธ๋ฆฌ์ root ๋ถํฐ, ๊ฐ children ์ ๋ชจ๋ ๋ฐฉ๋ฌธํ๋ฉด์
1) children์ด ์๋ค๋ฉด,
์์ ์๋๊ฒ ํด๋๋ผ๋ฉด -> ์์ ๊ทธ๋๋ก ์ ์ง๋ฅผ ์ํด -1 ๋ฐํ
์์ ์๋๊ฒ ํ์ผ์ด๋ผ๋ฉด -> ์์๋ฅผ ๋ฐ๊พธ๊ธฐ ์ํด 1 ๋ฐํ
๊ทธ๋ฆฌ๊ณ ๊ฐ children ์ ๋ํ์ฌ sortTree() ๋ฅผ ์ฌ๊ท์ ์ผ๋ก ํธ์ถํด์ค๋ค. ํ์ฌ ๋ ธ๋ ํ์์ ์๋ ์ด๋ค ํด๋ ๋ด๋ถ์ ํ์ผ๋ค๋ ๋ชจ๋ ์ ๋ ฌ๋ ํ์๊ฐ ์๊ธฐ ๋๋ฌธ์ด๋ค.
2) children์ด ์๋ค๋ฉด, ์๋ฌด์ผ๋ ์ผ์ด๋์ง ์๋๋ค.
const sortTree = (root: TreeNode)=>{
if(root.children){
root.children.sort((a, b) => {
if(a.type != b.type){
return a.type==='folder'?-1:1;
}
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
root.children.forEach((c)=>{
sortTree(c);
})
}
}
ํ์ผ ์ถ๊ฐํ๊ธฐ
์ฐ์ , ํ์ผ์ ์ถ๊ฐํด์ฃผ๊ธฐ ์ํ input ๊ณผ ์ ์ถ์ ์ํ button ํ๊ทธ๋ฅผ ๊ฐ๊ฐ ์ถ๊ฐํด์ค๋ค.
<input type="file" ref={inputRef}/>
<button onClick={addFile}>add File</button>
๊ทธ ๋ค์, ๊ธฐ์กด file ๋ชฉ๋ก์ contextAPI ๋ก ์ ์ญ์ ๋ฟ๋ฆฌ๊ณ , addFile ํธ์ถ์์ file ์ ๋ณด๋ฅผ ๋ฐ์์์ allFiles ์ ์ถ๊ฐํด์ฃผ๋ฉด ๋๋ค.
export const FilePane = () => {
const { allFiles,setAllFiles } = useWorkspaceContext();
const root = buildFileTree (allFiles);
sortTree(root);
console.log(root);
const inputRef = useRef(null);
const addFile = async (e) => {
console.log("addFile");
//e.preventDefault();
const files = inputRef.current.files;
const file = files[0];
console.log(file);
const path = file.name;
const contents : string = await file.text();
const newFile : File = {path,contents};
setAllFiles([...allFiles,newFile]);
}
return (<Box>
<Box p={1}>
<Typography variant="h6">Files</Typography>
<input type="file" ref={inputRef}/>
<button onClick={addFile}>add File</button>
{root.children?.map((c:TreeNode)=><FileNode depth={0} root={c} key={c.name}/>)}
</Box>
</Box>)
}
๋ํ, ๋๋ ํ ์คํธ์ฉ์ผ๋ก .cpp ํ์ผ์ ์ฌ์ฉํ๋๋ฐ ์ด๋ ๊ฒ ์๋กญ๊ฒ ์ฌ๋ผ๊ฐ ํ์ผ ํ์ฅ์๋ ์๋ํฐ ์์์ ์์๊ฒ ๋ณด์ผ ์ ์๋๋ก,
์๋ ์ฝ๋๋ฅผ ์ถ๊ฐํด์ค๋ค.
else if(['.c','.cpp'].some((extension)=>filePath.endsWith(extension))){
return 'c'
}
์ ์ฒด์ ์ธ ๋ชจ์ต์ ๋ค์๊ณผ ๊ฐ๋ค!
import React from 'react'
import { Box } from '@mui/material'
import { useWorkspaceContext } from '../Workspace/WorkspaceContext'
import MonacoEditor from "@monaco-editor/react";
import * as monaco from 'monaco-editor'
// in lieu of having actual Language Workers to determine the lang from a file path, we'll hard-code some handlers here
const getLanguageFromFilePath = (filePath: string) => {
if (filePath.endsWith('.html')) {
return 'html'
} else if (filePath.endsWith('.css')) {
return 'css'
} else if (['.js', '.jsx', '.ts', '.tsx'].some((extension) => filePath.endsWith(extension))) {
return 'javascript'
}
else if(['.c','.cpp'].some((extension)=>filePath.endsWith(extension))){
return 'c'
}
return 'plaintext'
}
export const Editor = () => {
const { activeFile } = useWorkspaceContext()
if (activeFile == null) return null
return (
<Box id='editor' flex={1}>
<MonacoEditor
value={activeFile?.contents}
language={getLanguageFromFilePath(activeFile?.path)}
/>
</Box>
)
}
์ต์ข ๊ฒฐ๊ณผ
๋ก์ปฌ์์ ํ์ผ์ ์ ํํ๊ณ ์ด๋ฅผ ์ ๋ก๋ํ์ฌ ์๋ํฐ ์ฐฝ์์ ํ์ธํ ์ ์๋ค!
'ํ๋ก ํธ์๋' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
์น ํ๋ก ํธ์๋ ํด๋ฆฐ ์ํคํ ์ณ(Clean Architecture) ์ ์ญ์ฌ (0) | 2025.04.29 |
---|