이제 메뉴 추가 화면을 구현해보자. 대략 아래와 같은 틀을 가진다.
(참고로 아직 서버에 API 개발은 안했으므로, 테스트 데이터를 만들어서 개발 진행했음)
각각의 영역은 svelte-material-ui 의 Card
컴포넌트를 사용했다.
https://sveltematerialui.com/demo/card
뭐 사실 입력할게 별로 없기 때문에 하나로 묶어도 되긴 하는데, 그냥 나누고 싶었다.
메뉴 데이터 자체가 트리 구조이다 보니, 부모 메뉴 선택이 좀 까다로워 보여서 별도로 분리한 측면이 크다.
전체 메뉴 데이터 중에서 부모 메뉴를 선택할 수도 있고, 안 할 수도 있다. (선택 안하는 경우는 자연스럽게 최상위 뎁스에 위치)
메뉴 데이터는 트리 구조이며, 테스트 데이터를 보면 어떤 구조인지 명확하게 알 수 있다.
// temp data
const menus = [
{name: 'menu 1', _id: 'menu 1'},
{name: 'menu 2', _id: 'menu 2', children: [
{name: 'menu 2 1', _id: 'menu 2 1', parent: 'menu 2'},
{name: 'menu 2 2', _id: 'menu 2 2', parent: 'menu 2'},
{name: 'menu 2 3', _id: 'menu 2 3', parent: 'menu 2', children: [
{name: 'menu 2 3 1', _id: 'menu 2 3 1', parent: 'menu 2 3'},
{name: 'menu 2 3 2', _id: 'menu 2 3 2', parent: 'menu 2 3'},
{name: 'menu 2 3 3', _id: 'menu 2 3 3', parent: 'menu 2 3', children: [
{name: 'menu 2 3 3 1', _id: 'menu 2 3 3 1', parent: 'menu 2 3 3'},
{name: 'menu 2 3 3 2', _id: 'menu 2 3 3 2', parent: 'menu 2 3 3'},
{name: 'menu 2 3 3 3', _id: 'menu 2 3 3 3', parent: 'menu 2 3 3'},
]},
]},
]},
{name: 'menu 3', _id: 'menu 3'},
];
부모 메뉴는 어느 depth 의 메뉴든지 선택이 가능하며, 메뉴마다 children 들이 있을 수도 있고 없을 수도 있기 때문에,
select box 에서 메뉴를 선택했을때 하위 depth 의 select box 를 모두 리셋하고, 추가로 선택 메뉴의 children 이 있을 때 그 다음 depth 의 select box 를 생성 & 노출한다.
뭐 말이 어려운데, 아래 캡쳐를 보면 금방 알 수 있다.
사용자가 선택한 메뉴에 따라 select box 를 생성 & 노출해야 하기 때문에, svelte-material-ui 의 Select
컴포넌트를 wrapping 한 컴포넌트를 별도로 만들어서 사용했다.
https://sveltematerialui.com/demo/select
MenuSelect.svelte
<Select key={(menu) => `${(menu && menu._id) || ''}`} bind:value={value} label={`${depth + 1}뎁스`} >
<Option value='선택 안함' />
{#each menus as menu}
<Option value={menu}>{menu.name}</Option>
{/each}
</Select>
<script>
import Select, { Option } from '@smui/select';
export let menus;
export let depth;
export let change;
let value;
$: change(value, depth);
</script>
코드는 직관적이라 참고할만한 문법은 아래 정도인 것 같다.
key={(menu).....
부분Select
컴포넌트 가이드 문서를 확인export
의 의미export
는 부모로부터 값을 전달 받을때 사용$: ...
의 의미Vue.js
의 computed
와 watch
와 비슷한 녀석...
내 참조하고 있는 변수의 값이 변경되면 해당 구문이 실행value
의 값이 변경될때마다 - select box 내에서 메뉴를 선택할때마다 실행이제 MenuSelect
컴포넌트를 쓰도록 AdminMenu.svelte
를 수정하자.
AdminMenu.svelte
에서는 아래 임무를 수행하는 코드를 추가한다.
일단 코드 부터 보자. (중요한 부분만 언급하며, 전체 코드는 GitHub 에서 확인)
AdminMenu.svelte
<div class="card-container">
<h3>부모 메뉴 선택</h3>
<Card variant="outlined">
<Content>
<div class="card-content-container">
{#each parents as parent, i}
<div>
<MenuSelect menus={parent.list} depth={i} change={changeParent}></MenuSelect>
</div>
{/each}
</div>
<pre class="status"><h2>선택된 부모 메뉴 : { selectedParents() }</h2></pre>
</Content>
<Actions>
<Button on:click={ () => init("parent") } variant="raised"><Label>초기화</Label></Button>
</Actions>
</Card>
</div>
<script>
import { push } from 'svelte-spa-router';
import Button, { Label } from '@smui/button';
import Card, { Content, Actions } from '@smui/card';
import Textfield from '@smui/textfield';
import HelperText from '@smui/textfield/helper-text';
import MenuSelect from './MenuSelect.svelte';
// temp data
const menus = [......];
let parents = [{list: menus, select: null}];
let name = '';
const changeParent = (value, depth) => {
if (!value) {
return;
}
parents = parents.slice(0, depth + 1);
parents[depth].select = value;
if (value.children && value.children.length > 0) {
parents.push({list: [...value.children], select: null});
}
};
</script>
위에서 언급한대로 svelte-material-ui 의 Card
, Content
, Actions
등의 컴포넌트를 사용했으며, 자세한 내용은 가이드 문서 참고하면 된다.
중요한 점은 parents
에 따라 MenuSelect
가 생성 & 노출된다는 것과 MenuSelect
에 changeParent
함수를 전달하고 통해 메뉴가 선택될때 MenuSelect
에서 changeParent
를 호출함으로서 부모에서 관리중인 parents
를 조작한다는 점이다.
그 외에 문법적으로 특이한 부분은 없어 보인다.
사실 현재 입력할만한 데이터가 name 밖에 없기 때문에, 크게 복잡할 건 없다.
부모 선택 영역과 마찬가지로 svelte-material-ui 의 Card
를 쓰며, 그 외 input 을 위해 Textfield
, HelperText
등의 컴포넌트를 사용했다. (자세한 내용은 아래 링크 참조)
https://sveltematerialui.com/demo/textfield
코드가 길지 않아 AdminMenu.svelte
에 그냥 추가했다. (전체 코드는 GitHub 에서 확인)
AdminMenu.svelte
<div class="card-container">
<h3>메뉴 정보 입력</h3>
<Card variant="outlined">
<Content>
<div class="card-content-container">
<div>
<Textfield bind:value={name} label="메뉴명" required>
<HelperText slot="helper">메뉴명을 입력해주세용.</HelperText>
</Textfield>
</div>
</div>
</Content>
<Actions>
<Button on:click={ () => init("info") } variant="raised"><Label>초기화</Label></Button>
</Actions>
</Card>
</div>
<script>
import { push } from 'svelte-spa-router';
import Button, { Label } from '@smui/button';
import Card, { Content, Actions } from '@smui/card';
import Textfield from '@smui/textfield';
import HelperText from '@smui/textfield/helper-text';
let name = '';
</script>
대충 화면 틀은 완성했다. (짜잘한 부분은 일단 넘어가자.)
이제는 실제 API 부분을 개발 진행해보자.