Skip to content

Commit

Permalink
Merge pull request #344 from Twinsteak/feat/config-import
Browse files Browse the repository at this point in the history
[feat] 설정 데이터 내보내기/가져오기
  • Loading branch information
EATSTEAK authored Jul 16, 2024
2 parents 09f0f94 + 6919b44 commit ce7c090
Show file tree
Hide file tree
Showing 12 changed files with 475 additions and 28 deletions.
31 changes: 31 additions & 0 deletions packages/client/src/components/atom/form/TextArea.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script lang="ts">
export let id: string;
export let value: string;
export let label: string;
export let showLabel: boolean = false;
export let labelClass: string = '';
export let inputClass: string = '';
export let invalidClass: string = '';
export let invalidText: string = '이 값은 올바르지 않습니다.';
let clazz = '';
export { clazz as class };
</script>

<div class="{clazz} flex flex-col">
<!--suppress XmlInvalidId -->
<label class="{labelClass} mb-1 block font-bold" for={id} class:hidden={!showLabel}
>{label ?? ''} <span class:hidden={!$$props.required} class="text-red-800">*</span></label>
<textarea
{id}
class="{inputClass} resize-none grow rounded-md border-transparent bg-gray-100 transition-all
invalid:outline-red-600 hover:bg-gray-200 focus:bg-white disabled:bg-gray-300 disabled:text-gray-400"
bind:value
{...$$restProps} />
<p class="{invalidClass} mt-1 hidden">{invalidText}</p>
</div>

<style lang="postcss">
input:invalid ~ p {
@apply block;
}
</style>
2 changes: 1 addition & 1 deletion packages/client/src/components/molecule/Modal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
<Dismiss />
</button>
</div>
<div class="mx-4 my-2 h-auto grow overflow-y-auto overflow-x-visible">
<div class="mx-4 my-2 h-auto grow overflow-y-auto overflow-x-visible p-1">
<slot />
</div>
<div class="mx-4 flex shrink-0 justify-end gap-3 pb-4">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<script lang="ts">
import Modal from '../../Modal.svelte';
import { config } from '$lib/store';
import DocumentArrowRight from '../../../../icons/DocumentArrowRight.svelte';
import { createEventDispatcher } from 'svelte';
import { DepartmentConfigSchema, getDepartmentConfigs } from '$lib/api/config';
import TextGrammarCheckmark from '../../../../icons/TextGrammarCheckmark.svelte';
import TextArea from 'src/components/atom/form/TextArea.svelte';
import { z } from 'zod';
import Checkmark from 'src/icons/Checkmark.svelte';
import Dismiss from 'src/icons/Dismiss.svelte';
const dispatch = createEventDispatcher<{
submit: DepartmentConfigResponse[];
}>();
$: departments = $config && $config.success ? getDepartmentConfigs($config.result) : [];
export let open = false;
const title = '학과(부) 설정 가져오기/내보내기';
let configText;
$: if(departments && !configText) {
configText = JSON.stringify(departments, null, 2);
}
let green = false;
let error = '';
$: if(configText) {
error = '';
green = false;
}
function checkConfigJSON(): boolean {
try {
const parsed = z.array(DepartmentConfigSchema).safeParse(JSON.parse(configText));
if(parsed.success == false) {
error = JSON.stringify(parsed.error, null, 2);
}
return parsed.success;
} catch (e) {
console.error(e.message);
error = e.message;
return false;
}
}
function updateConfigs() {
const parsed = z.array(DepartmentConfigSchema).safeParse(JSON.parse(configText));
if(parsed.success) {
const configs = parsed.data;
const configResponse: DepartmentConfigResponse[] = configs.map((conf) => {
const { id, name, activateFrom, activateTo, contact } = conf;
return {
id,
name,
...(activateFrom
? { activate_from: activateFrom.toISOString() }
: { activate_from: null }),
...(activateTo
? { activate_to: activateTo.toISOString() }
: { activate_to: null }),
...(contact ? { contact } : { contact: null }),
};
});
dispatch('submit', configResponse);
}
}
</script>

<Modal
on:close
on:click:secondary={() => green = checkConfigJSON()}
on:click={updateConfigs}
{title}
bind:open
primaryDisabled={!green}
primaryText="가져오기"
secondaryText="검증"
isPrimaryBtnIconRight
isSecondaryBtnIconRight
{...$$restProps}>
<div class="flex flex-col gap-3 h-full">
<TextArea
class="h-full"
id="json"
bind:value={configText}
label="서비스 설정 데이터"
showLabel
/>
<p class="text-gray-800 text-sm">데이터를 복사하여 저장하거나, 직접 수정하여 서비스에 적용할 수 있습니다.</p>
{#if green}
<div class="text-green-800 flex items-center gap-1">
<Checkmark class="w-3" />
<p class="text-xs font-bold">검증 성공!</p>
</div>
{:else if error}
<div class="text-red-800 flex items-center gap-1">
<Dismiss class="w-3" />
<p class="text-xs font-bold">검증 실패</p>
</div>
<pre class="rounded-md p-1 bg-white text-red-800 whitespace-pre-wrap text-xs font-mono">{error}</pre>
{:else}
<p class="text-xs">데이터를 가져오려면 검증하세요.</p>
{/if}
</div>
<DocumentArrowRight slot="primaryIcon" />
<TextGrammarCheckmark slot="secondaryIcon" />
</Modal>
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@
import DepartmentConfigEditor from './DepartmentConfigEditor.svelte';
import Skeleton from '../../../atom/Skeleton.svelte';
import { apiDeleteConfig, apiUpdateConfig, getDepartmentConfigs } from '$lib/api/config';
import Code from '../../../../icons/Code.svelte';
import DepartmentImportExportModal from './DepartmentImportExportModal.svelte';
import Button from '../../../atom/Button.svelte';
$: configs = $config && $config.success ? getDepartmentConfigs($config.result) : [];
let updating = false;
$: if ($config) updating = false;
let importExportModalOpen = false;
function openImportExportModal() {
importExportModalOpen = true;
}
function deleteDepartment(event: CustomEvent<ConfigDeleteRequest>) {
updating = true;
Expand Down Expand Up @@ -43,10 +51,40 @@
updating = false;
});
}
function importDepartmentConfigs(event: CustomEvent<DepartmentConfigResponse[]>) {
importExportModalOpen = false;
const importedConfig = event.detail;
updating = true;
const updatePromises = importedConfig.map((conf) => apiUpdateConfig(conf as DepartmentConfigUpdateRequest).then((res) => {
if(res.success == false) {
throw (res as ErrorResponse<LockerError>).error;
}
return true;
}));
Promise.all(updatePromises)
.then(() => {
updating = false;
config.refresh();
})
.catch((err) => {
console.error(err);
updating = false;
});
}
</script>

<div class="my-8 flex flex-col gap-3 lg:mx-8">
<h3 class="mx-6 lg:mx-0">학과(부)별 설정</h3>
<div class="my-8 flex w-auto flex-col item-stretch gap-3 lg:mx-8">
<div class="mx-6 flex w-full flex-wrap lg:mx-0">
<h3>학과(부)별 설정</h3>
{#if !updating}
<div class="flex grow items-center justify-end gap-1">
<Button on:click={openImportExportModal} class="bg-gray-200 text-gray-700" isIconRight>
<Code slot="icon" />
</Button>
</div>
{/if}
</div>
{#if $config && $config.success && !updating}
<div class="flex flex-col gap-3 bg-white p-6 shadow-md lg:rounded-md">
<DepartmentConfigEditor on:delete={deleteDepartment} on:update={updateDepartment} {configs} />
Expand All @@ -57,3 +95,8 @@
<Skeleton class="h-[32rem] w-full grow bg-gray-200 lg:rounded-md" />
{/if}
</div>

<DepartmentImportExportModal
bind:open={importExportModalOpen}
on:close={() => (importExportModalOpen = false)}
on:submit={importDepartmentConfigs} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<script lang="ts">
import Modal from '../../Modal.svelte';
import { config } from '$lib/store';
import DocumentArrowRight from '../../../../icons/DocumentArrowRight.svelte';
import { createEventDispatcher } from 'svelte';
import { ServiceConfigSchema, getServiceConfig } from '$lib/api/config';
import TextGrammarCheckmark from '../../../../icons/TextGrammarCheckmark.svelte';
import TextArea from 'src/components/atom/form/TextArea.svelte';
import Checkmark from 'src/icons/Checkmark.svelte';
import Dismiss from 'src/icons/Dismiss.svelte';
const dispatch = createEventDispatcher<{
submit: ServiceConfigResponse;
}>();
$: serviceConfig = $config && $config.success ? getServiceConfig($config.result) : {};
export let open = false;
const title = '서비스 설정 가져오기/내보내기';
let configText;
$: if(serviceConfig && !configText) {
configText = JSON.stringify(serviceConfig, null, 2);
}
let green = false;
let error = '';
$: if(configText) {
error = '';
green = false;
}
function checkConfigJSON(): boolean {
try {
const parsed = ServiceConfigSchema.safeParse(JSON.parse(configText));
if(parsed.success == false) {
error = JSON.stringify(parsed.error, null, 2);
}
return parsed.success;
} catch (e) {
console.error(e.message);
error = e.message;
return false;
}
}
function updateConfigs() {
const parsed = ServiceConfigSchema.safeParse(JSON.parse(configText));
if(parsed.success) {
const { name, activateFrom, activateTo, alert, buildings } = parsed.data as ServiceConfig;
const configResponse: ServiceConfigResponse = {
id: 'SERVICE',
name,
...(activateFrom
? { activate_from: activateFrom.toISOString() }
: { activate_from: null }),
...(activateTo
? { activate_to: activateTo.toISOString() }
: { activate_to: null }),
...(alert ? { alert } : { alert: null }),
buildings,
}
dispatch('submit', configResponse);
}
}
</script>

<Modal
on:close
on:click:secondary={() => green = checkConfigJSON()}
on:click={updateConfigs}
{title}
bind:open
primaryDisabled={!green}
primaryText="가져오기"
secondaryText="검증"
isPrimaryBtnIconRight
isSecondaryBtnIconRight
{...$$restProps}>
<div class="flex flex-col gap-3 h-full">
<TextArea
class="h-full"
id="json"
bind:value={configText}
label="서비스 설정 데이터"
showLabel
/>
<p class="text-gray-800 text-sm">데이터를 복사하여 저장하거나, 직접 수정하여 서비스에 적용할 수 있습니다.</p>
{#if green}
<div class="text-green-800 flex items-center gap-1">
<Checkmark class="w-3" />
<p class="text-xs font-bold">검증 성공!</p>
</div>
{:else if error}
<div class="text-red-800 flex items-center gap-1">
<Dismiss class="w-3" />
<p class="text-xs font-bold">검증 실패</p>
</div>
<pre class="rounded-md p-1 bg-white text-red-800 whitespace-pre-wrap text-xs font-mono">{error}</pre>
{:else}
<p class="text-xs">데이터를 가져오려면 검증하세요.</p>
{/if}
</div>
<DocumentArrowRight slot="primaryIcon" />
<TextGrammarCheckmark slot="secondaryIcon" />
</Modal>
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import UpdateScreen from '../../../atom/UpdateScreen.svelte';
import Warning from '../../../../icons/Warning.svelte';
import Skeleton from '../../../atom/Skeleton.svelte';
import Code from 'src/icons/Code.svelte';
import ServiceImportExportModal from './ServiceImportExportModal.svelte';
let updating = false;
Expand All @@ -19,6 +21,11 @@
let activateTo: Date;
let alert: string;
let buildings: { [buildingNum: string]: Building };
let importExportModalOpen = false;
function openImportExportModal() {
importExportModalOpen = true;
}
$: serviceConfig = $config && $config.success ? getServiceConfig($config.result) : undefined;
Expand Down Expand Up @@ -66,6 +73,25 @@
});
}
function importServiceConfig(event: CustomEvent<ServiceConfigResponse>) {
importExportModalOpen = false;
const importedConfig = event.detail;
updating = true;
apiUpdateConfig(importedConfig as ServiceConfigUpdateRequest)
.then((res) => {
updating = false;
if (res.success) {
config.refresh();
} else {
console.error((res as ErrorResponse<LockerError>).error);
}
})
.catch((err) => {
console.error(err);
updating = false;
});
}
$: if (serviceConfig) {
initializeValues();
updating = false;
Expand Down Expand Up @@ -102,6 +128,12 @@
저장
<SaveEdit slot="icon" />
</Button>
<Button
on:click={openImportExportModal}
class="bg-gray-200 text-gray-700"
isIconRight>
<Code slot="icon" />
</Button>
</div>
</div>
{#if serviceConfig && !isServiceReady}
Expand Down Expand Up @@ -193,3 +225,8 @@
</section>
{/if}
</div>

<ServiceImportExportModal
bind:open={importExportModalOpen}
on:close={() => (importExportModalOpen = false)}
on:submit={importServiceConfig} />
Loading

0 comments on commit ce7c090

Please sign in to comment.